scroll-snap-kit 1.0.0 → 2.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/LICENSE +21 -0
- package/README.md +302 -36
- package/dist/index.cjs.js +913 -0
- package/dist/index.d.ts +281 -0
- package/dist/index.esm.js +885 -0
- package/dist/index.umd.js +779 -0
- package/package.json +28 -13
- package/src/index.d.ts +281 -0
- package/src/index.js +11 -0
- package/src/utils.js +589 -0
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scroll-snap-kit v2.0.0 (CommonJS)
|
|
3
|
+
* MIT License — https://github.com/farazfarid/scroll-snap-kit
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* scroll-snap-kit — Core Utilities
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Smoothly scrolls to a target element or position.
|
|
13
|
+
* @param {Element|number} target - A DOM element or Y pixel value
|
|
14
|
+
* @param {{ behavior?: ScrollBehavior, block?: ScrollLogicalPosition, offset?: number }} options
|
|
15
|
+
*/
|
|
16
|
+
function scrollTo(target, options = {}) {
|
|
17
|
+
const { behavior = 'smooth', block = 'start', offset = 0 } = options;
|
|
18
|
+
|
|
19
|
+
if (typeof target === 'number') {
|
|
20
|
+
window.scrollTo({ top: target + offset, behavior });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!(target instanceof Element)) {
|
|
25
|
+
console.warn('[scroll-snap-kit] scrollTo: target must be an Element or a number');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (offset !== 0) {
|
|
30
|
+
const y = target.getBoundingClientRect().top + window.scrollY + offset;
|
|
31
|
+
window.scrollTo({ top: y, behavior });
|
|
32
|
+
} else {
|
|
33
|
+
target.scrollIntoView({ behavior, block });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Smoothly scrolls to the top of the page.
|
|
39
|
+
* @param {{ behavior?: ScrollBehavior }} options
|
|
40
|
+
*/
|
|
41
|
+
function scrollToTop(options = {}) {
|
|
42
|
+
const { behavior = 'smooth' } = options;
|
|
43
|
+
window.scrollTo({ top: 0, behavior });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Smoothly scrolls to the bottom of the page.
|
|
48
|
+
* @param {{ behavior?: ScrollBehavior }} options
|
|
49
|
+
*/
|
|
50
|
+
function scrollToBottom(options = {}) {
|
|
51
|
+
const { behavior = 'smooth' } = options;
|
|
52
|
+
window.scrollTo({ top: document.body.scrollHeight, behavior });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns the current scroll position and scroll percentage.
|
|
57
|
+
* @param {Element} [container=window] - Optional scrollable container
|
|
58
|
+
* @returns {{ x: number, y: number, percentX: number, percentY: number }}
|
|
59
|
+
*/
|
|
60
|
+
function getScrollPosition(container) {
|
|
61
|
+
if (container && container instanceof Element) {
|
|
62
|
+
const { scrollLeft, scrollTop, scrollWidth, scrollHeight, clientWidth, clientHeight } = container;
|
|
63
|
+
return {
|
|
64
|
+
x: scrollLeft,
|
|
65
|
+
y: scrollTop,
|
|
66
|
+
percentX: scrollWidth > clientWidth ? Math.round((scrollLeft / (scrollWidth - clientWidth)) * 100) : 0,
|
|
67
|
+
percentY: scrollHeight > clientHeight ? Math.round((scrollTop / (scrollHeight - clientHeight)) * 100) : 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const x = window.scrollX;
|
|
72
|
+
const y = window.scrollY;
|
|
73
|
+
const maxX = document.body.scrollWidth - window.innerWidth;
|
|
74
|
+
const maxY = document.body.scrollHeight - window.innerHeight;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
x,
|
|
78
|
+
y,
|
|
79
|
+
percentX: maxX > 0 ? Math.round((x / maxX) * 100) : 0,
|
|
80
|
+
percentY: maxY > 0 ? Math.round((y / maxY) * 100) : 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Attaches a throttled scroll event listener.
|
|
86
|
+
* @param {(position: ReturnType<typeof getScrollPosition>) => void} callback
|
|
87
|
+
* @param {{ throttle?: number, container?: Element }} options
|
|
88
|
+
* @returns {() => void} Cleanup function to remove the listener
|
|
89
|
+
*/
|
|
90
|
+
function onScroll(callback, options = {}) {
|
|
91
|
+
const { throttle: throttleMs = 100, container } = options;
|
|
92
|
+
const target = container || window;
|
|
93
|
+
|
|
94
|
+
let ticking = false;
|
|
95
|
+
let lastTime = 0;
|
|
96
|
+
|
|
97
|
+
const handler = () => {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
if (now - lastTime < throttleMs) {
|
|
100
|
+
if (!ticking) {
|
|
101
|
+
ticking = true;
|
|
102
|
+
requestAnimationFrame(() => {
|
|
103
|
+
callback(getScrollPosition(container));
|
|
104
|
+
ticking = false;
|
|
105
|
+
lastTime = Date.now();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
lastTime = now;
|
|
111
|
+
callback(getScrollPosition(container));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
target.addEventListener('scroll', handler, { passive: true });
|
|
115
|
+
return () => target.removeEventListener('scroll', handler);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Checks whether an element is currently visible in the viewport.
|
|
120
|
+
* @param {Element} element
|
|
121
|
+
* @param {{ threshold?: number }} options - threshold: 0–1, portion of element that must be visible
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
function isInViewport(element, options = {}) {
|
|
125
|
+
if (!(element instanceof Element)) {
|
|
126
|
+
console.warn('[scroll-snap-kit] isInViewport: argument must be an Element');
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { threshold = 0 } = options;
|
|
131
|
+
const rect = element.getBoundingClientRect();
|
|
132
|
+
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
133
|
+
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
134
|
+
|
|
135
|
+
const verticalVisible = rect.top + rect.height * threshold < windowHeight && rect.bottom - rect.height * threshold > 0;
|
|
136
|
+
const horizontalVisible = rect.left + rect.width * threshold < windowWidth && rect.right - rect.width * threshold > 0;
|
|
137
|
+
|
|
138
|
+
return verticalVisible && horizontalVisible;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Locks the page scroll (e.g. when a modal is open).
|
|
143
|
+
*/
|
|
144
|
+
function lockScroll() {
|
|
145
|
+
const scrollY = window.scrollY;
|
|
146
|
+
document.body.style.position = 'fixed';
|
|
147
|
+
document.body.style.top = `-${scrollY}px`;
|
|
148
|
+
document.body.style.width = '100%';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Unlocks the page scroll and restores position.
|
|
153
|
+
*/
|
|
154
|
+
function unlockScroll() {
|
|
155
|
+
const scrollY = document.body.style.top;
|
|
156
|
+
document.body.style.position = '';
|
|
157
|
+
document.body.style.top = '';
|
|
158
|
+
document.body.style.width = '';
|
|
159
|
+
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─────────────────────────────────────────────
|
|
163
|
+
// NEW FEATURES
|
|
164
|
+
// ─────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* scrollSpy — watches scroll position and highlights nav links
|
|
168
|
+
* matching the currently active section.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} sectionsSelector CSS selector for the sections to spy on
|
|
171
|
+
* @param {string} linksSelector CSS selector for the nav links
|
|
172
|
+
* @param {{ offset?: number, activeClass?: string }} options
|
|
173
|
+
* @returns {() => void} cleanup / stop function
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* const stop = scrollSpy('section[id]', 'nav a', { offset: 80, activeClass: 'active' })
|
|
177
|
+
*/
|
|
178
|
+
function scrollSpy(sectionsSelector, linksSelector, options = {}) {
|
|
179
|
+
const { offset = 0, activeClass = 'scroll-spy-active' } = options;
|
|
180
|
+
|
|
181
|
+
const sections = Array.from(document.querySelectorAll(sectionsSelector));
|
|
182
|
+
const links = Array.from(document.querySelectorAll(linksSelector));
|
|
183
|
+
|
|
184
|
+
if (!sections.length || !links.length) {
|
|
185
|
+
console.warn('[scroll-snap-kit] scrollSpy: no sections or links found');
|
|
186
|
+
return () => { };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function update() {
|
|
190
|
+
const scrollY = window.scrollY + offset;
|
|
191
|
+
let current = sections[0];
|
|
192
|
+
|
|
193
|
+
for (const section of sections) {
|
|
194
|
+
if (section.offsetTop <= scrollY) current = section;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
links.forEach(link => {
|
|
198
|
+
link.classList.remove(activeClass);
|
|
199
|
+
const href = link.getAttribute('href');
|
|
200
|
+
if (href && current && href === `#${current.id}`) {
|
|
201
|
+
link.classList.add(activeClass);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
update();
|
|
207
|
+
window.addEventListener('scroll', update, { passive: true });
|
|
208
|
+
return () => window.removeEventListener('scroll', update);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* onScrollEnd — fires a callback once the user stops scrolling.
|
|
213
|
+
*
|
|
214
|
+
* @param {() => void} callback
|
|
215
|
+
* @param {{ delay?: number, container?: Element }} options
|
|
216
|
+
* @returns {() => void} cleanup function
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* const stop = onScrollEnd(() => console.log('Scrolling stopped!'), { delay: 150 })
|
|
220
|
+
*/
|
|
221
|
+
function onScrollEnd(callback, options = {}) {
|
|
222
|
+
const { delay = 150, container } = options;
|
|
223
|
+
const target = container || window;
|
|
224
|
+
let timer = null;
|
|
225
|
+
|
|
226
|
+
const handler = () => {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
timer = setTimeout(() => {
|
|
229
|
+
callback(getScrollPosition(container));
|
|
230
|
+
}, delay);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
target.addEventListener('scroll', handler, { passive: true });
|
|
234
|
+
return () => {
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
target.removeEventListener('scroll', handler);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* scrollIntoViewIfNeeded — scrolls to an element only if it is
|
|
242
|
+
* partially or fully outside the visible viewport.
|
|
243
|
+
*
|
|
244
|
+
* @param {Element} element
|
|
245
|
+
* @param {{ behavior?: ScrollBehavior, offset?: number, threshold?: number }} options
|
|
246
|
+
* threshold: 0–1, how much of the element must be visible before we skip scrolling (default 1 = fully visible)
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* scrollIntoViewIfNeeded(document.querySelector('.card'))
|
|
250
|
+
*/
|
|
251
|
+
function scrollIntoViewIfNeeded(element, options = {}) {
|
|
252
|
+
if (!(element instanceof Element)) {
|
|
253
|
+
console.warn('[scroll-snap-kit] scrollIntoViewIfNeeded: argument must be an Element');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const { behavior = 'smooth', offset = 0, threshold = 1 } = options;
|
|
258
|
+
const rect = element.getBoundingClientRect();
|
|
259
|
+
const wh = window.innerHeight || document.documentElement.clientHeight;
|
|
260
|
+
const ww = window.innerWidth || document.documentElement.clientWidth;
|
|
261
|
+
|
|
262
|
+
const visibleH = Math.min(rect.bottom, wh) - Math.max(rect.top, 0);
|
|
263
|
+
const visibleW = Math.min(rect.right, ww) - Math.max(rect.left, 0);
|
|
264
|
+
const visibleRatio =
|
|
265
|
+
(Math.max(0, visibleH) * Math.max(0, visibleW)) / (rect.height * rect.width);
|
|
266
|
+
|
|
267
|
+
if (visibleRatio >= threshold) return; // already sufficiently visible — skip
|
|
268
|
+
|
|
269
|
+
const y = rect.top + window.scrollY - offset;
|
|
270
|
+
window.scrollTo({ top: y, behavior });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Built-in easing functions for use with easeScroll().
|
|
275
|
+
*/
|
|
276
|
+
const Easings = {
|
|
277
|
+
linear: (t) => t,
|
|
278
|
+
easeInQuad: (t) => t * t,
|
|
279
|
+
easeOutQuad: (t) => t * (2 - t),
|
|
280
|
+
easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
|
|
281
|
+
easeInCubic: (t) => t * t * t,
|
|
282
|
+
easeOutCubic: (t) => (--t) * t * t + 1,
|
|
283
|
+
easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
|
|
284
|
+
easeInQuart: (t) => t * t * t * t,
|
|
285
|
+
easeOutQuart: (t) => 1 - (--t) * t * t * t,
|
|
286
|
+
easeOutElastic: (t) => {
|
|
287
|
+
const c4 = (2 * Math.PI) / 3;
|
|
288
|
+
return t === 0 ? 0 : t === 1 ? 1
|
|
289
|
+
: Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
|
|
290
|
+
},
|
|
291
|
+
easeOutBounce: (t) => {
|
|
292
|
+
const n1 = 7.5625, d1 = 2.75;
|
|
293
|
+
if (t < 1 / d1) return n1 * t * t;
|
|
294
|
+
if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
|
|
295
|
+
if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
|
296
|
+
return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* easeScroll — scroll to a position with a custom easing curve,
|
|
302
|
+
* bypassing the browser's native smooth scroll.
|
|
303
|
+
*
|
|
304
|
+
* @param {Element|number} target DOM element or pixel Y value
|
|
305
|
+
* @param {{ duration?: number, easing?: (t: number) => number, offset?: number }} options
|
|
306
|
+
* @returns {Promise<void>} resolves when animation completes
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* await easeScroll('#contact', { duration: 800, easing: Easings.easeOutElastic })
|
|
310
|
+
*/
|
|
311
|
+
function easeScroll(target, options = {}) {
|
|
312
|
+
const { duration = 600, easing = Easings.easeInOutCubic, offset = 0 } = options;
|
|
313
|
+
|
|
314
|
+
let targetY;
|
|
315
|
+
if (typeof target === 'number') {
|
|
316
|
+
targetY = target + offset;
|
|
317
|
+
} else {
|
|
318
|
+
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
|
319
|
+
if (!el) { console.warn('[scroll-snap-kit] easeScroll: target not found'); return Promise.resolve(); }
|
|
320
|
+
targetY = el.getBoundingClientRect().top + window.scrollY + offset;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const startY = window.scrollY;
|
|
324
|
+
const distance = targetY - startY;
|
|
325
|
+
const startTime = performance.now();
|
|
326
|
+
|
|
327
|
+
return new Promise((resolve) => {
|
|
328
|
+
function step(now) {
|
|
329
|
+
const elapsed = now - startTime;
|
|
330
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
331
|
+
const easedProgress = easing(progress);
|
|
332
|
+
|
|
333
|
+
window.scrollTo(0, startY + distance * easedProgress);
|
|
334
|
+
|
|
335
|
+
if (progress < 1) {
|
|
336
|
+
requestAnimationFrame(step);
|
|
337
|
+
} else {
|
|
338
|
+
resolve();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
requestAnimationFrame(step);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ─────────────────────────────────────────────
|
|
346
|
+
// v1.2 FEATURES
|
|
347
|
+
// ─────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* scrollSequence — run multiple easeScroll animations one after another.
|
|
351
|
+
* Returns { promise, cancel } — cancel() aborts mid-sequence.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* const { promise, cancel } = scrollSequence([
|
|
355
|
+
* { target: '#intro', duration: 600 },
|
|
356
|
+
* { target: '#features', duration: 800, pause: 400 },
|
|
357
|
+
* { target: '#pricing', duration: 600, easing: Easings.easeOutElastic },
|
|
358
|
+
* ])
|
|
359
|
+
*/
|
|
360
|
+
function scrollSequence(steps) {
|
|
361
|
+
let cancelled = false;
|
|
362
|
+
const promise = (async () => {
|
|
363
|
+
for (const step of steps) {
|
|
364
|
+
if (cancelled) break;
|
|
365
|
+
const { target, duration = 600, easing = Easings.easeInOutCubic, offset = 0, pause = 0 } = step;
|
|
366
|
+
await easeScroll(target, { duration, easing, offset });
|
|
367
|
+
if (pause > 0 && !cancelled) await new Promise(res => setTimeout(res, pause));
|
|
368
|
+
}
|
|
369
|
+
})();
|
|
370
|
+
return { promise, cancel: () => { cancelled = true; } };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* parallax — attach a parallax scroll effect to one or more elements.
|
|
375
|
+
* speed < 1 = slower (background), speed > 1 = faster (foreground), speed < 0 = reverse.
|
|
376
|
+
* Returns a destroy / cleanup function.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* const destroy = parallax('.hero-bg', { speed: 0.4 })
|
|
380
|
+
* const destroy = parallax('.clouds', { speed: -0.2, axis: 'x' })
|
|
381
|
+
*/
|
|
382
|
+
function parallax(targets, options = {}) {
|
|
383
|
+
const { speed = 0.5, axis = 'y', container } = options;
|
|
384
|
+
let els;
|
|
385
|
+
if (typeof targets === 'string') els = Array.from(document.querySelectorAll(targets));
|
|
386
|
+
else if (targets instanceof Element) els = [targets];
|
|
387
|
+
else els = Array.from(targets);
|
|
388
|
+
if (!els.length) { console.warn('[scroll-snap-kit] parallax: no elements found'); return () => { }; }
|
|
389
|
+
|
|
390
|
+
const origins = els.map(el => ({
|
|
391
|
+
el,
|
|
392
|
+
originY: el.getBoundingClientRect().top + window.scrollY,
|
|
393
|
+
originX: el.getBoundingClientRect().left + window.scrollX,
|
|
394
|
+
}));
|
|
395
|
+
|
|
396
|
+
let rafId = null;
|
|
397
|
+
function update() {
|
|
398
|
+
const scrollY = container ? container.scrollTop : window.scrollY;
|
|
399
|
+
const scrollX = container ? container.scrollLeft : window.scrollX;
|
|
400
|
+
origins.forEach(({ el, originY, originX }) => {
|
|
401
|
+
const dy = (scrollY - (originY - window.innerHeight / 2)) * (speed - 1);
|
|
402
|
+
const dx = (scrollX - (originX - window.innerWidth / 2)) * (speed - 1);
|
|
403
|
+
if (axis === 'y') el.style.transform = `translateY(${dy}px)`;
|
|
404
|
+
else if (axis === 'x') el.style.transform = `translateX(${dx}px)`;
|
|
405
|
+
else el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
406
|
+
});
|
|
407
|
+
rafId = null;
|
|
408
|
+
}
|
|
409
|
+
const handler = () => { if (!rafId) rafId = requestAnimationFrame(update); };
|
|
410
|
+
const t = container || window;
|
|
411
|
+
t.addEventListener('scroll', handler, { passive: true });
|
|
412
|
+
update();
|
|
413
|
+
return () => {
|
|
414
|
+
t.removeEventListener('scroll', handler);
|
|
415
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
416
|
+
origins.forEach(({ el }) => { el.style.transform = ''; });
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* scrollProgress — track how far the user has scrolled through a specific element (0→1).
|
|
422
|
+
* 0 = element top just entered the viewport, 1 = element bottom just left the viewport.
|
|
423
|
+
* Returns a cleanup function.
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* const stop = scrollProgress('#article', progress => {
|
|
427
|
+
* bar.style.width = `${progress * 100}%`
|
|
428
|
+
* })
|
|
429
|
+
*/
|
|
430
|
+
function scrollProgress(element, callback, options = {}) {
|
|
431
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
432
|
+
if (!el) { console.warn('[scroll-snap-kit] scrollProgress: element not found'); return () => { }; }
|
|
433
|
+
const { offset = 0 } = options;
|
|
434
|
+
function calculate() {
|
|
435
|
+
const rect = el.getBoundingClientRect();
|
|
436
|
+
const wh = window.innerHeight;
|
|
437
|
+
const total = rect.height + wh;
|
|
438
|
+
const passed = wh - rect.top + offset;
|
|
439
|
+
callback(Math.min(1, Math.max(0, passed / total)));
|
|
440
|
+
}
|
|
441
|
+
calculate();
|
|
442
|
+
window.addEventListener('scroll', calculate, { passive: true });
|
|
443
|
+
window.addEventListener('resize', calculate);
|
|
444
|
+
return () => {
|
|
445
|
+
window.removeEventListener('scroll', calculate);
|
|
446
|
+
window.removeEventListener('resize', calculate);
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* snapToSection — after scrolling stops, auto-snap to the nearest section.
|
|
452
|
+
* Returns a destroy function.
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* const destroy = snapToSection('section[id]', { delay: 150, offset: -70 })
|
|
456
|
+
*/
|
|
457
|
+
function snapToSection(sections, options = {}) {
|
|
458
|
+
const { delay = 150, offset = 0, duration = 500, easing = Easings.easeInOutCubic } = options;
|
|
459
|
+
const els = typeof sections === 'string'
|
|
460
|
+
? Array.from(document.querySelectorAll(sections))
|
|
461
|
+
: Array.from(sections);
|
|
462
|
+
if (!els.length) { console.warn('[scroll-snap-kit] snapToSection: no sections found'); return () => { }; }
|
|
463
|
+
|
|
464
|
+
let timer = null, snapping = false;
|
|
465
|
+
const handler = () => {
|
|
466
|
+
clearTimeout(timer);
|
|
467
|
+
timer = setTimeout(async () => {
|
|
468
|
+
if (snapping) return;
|
|
469
|
+
snapping = true;
|
|
470
|
+
const scrollMid = window.scrollY + window.innerHeight / 2;
|
|
471
|
+
let closest = els[0], minDist = Infinity;
|
|
472
|
+
els.forEach(el => {
|
|
473
|
+
const mid = el.offsetTop + el.offsetHeight / 2;
|
|
474
|
+
const d = Math.abs(mid - scrollMid);
|
|
475
|
+
if (d < minDist) { minDist = d; closest = el; }
|
|
476
|
+
});
|
|
477
|
+
await easeScroll(closest, { duration, easing, offset });
|
|
478
|
+
snapping = false;
|
|
479
|
+
}, delay);
|
|
480
|
+
};
|
|
481
|
+
window.addEventListener('scroll', handler, { passive: true });
|
|
482
|
+
return () => { clearTimeout(timer); window.removeEventListener('scroll', handler); };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ─────────────────────────────────────────────
|
|
486
|
+
// v2.0 FEATURES
|
|
487
|
+
// ─────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* scrollReveal — animate elements in as they scroll into view.
|
|
491
|
+
* Supports fade, slide (up/down/left/right), scale, and combinations.
|
|
492
|
+
* Uses IntersectionObserver for performance.
|
|
493
|
+
* Returns a destroy function.
|
|
494
|
+
*
|
|
495
|
+
* @param {string|Element|Element[]} targets
|
|
496
|
+
* @param {{ effect?: 'fade'|'slide-up'|'slide-down'|'slide-left'|'slide-right'|'scale'|'fade-scale',
|
|
497
|
+
* duration?: number, delay?: number, easing?: string,
|
|
498
|
+
* threshold?: number, once?: boolean, distance?: string }} options
|
|
499
|
+
* @returns {() => void}
|
|
500
|
+
*
|
|
501
|
+
* @example
|
|
502
|
+
* const destroy = scrollReveal('.card', { effect: 'slide-up', duration: 600, delay: 100 })
|
|
503
|
+
*/
|
|
504
|
+
function scrollReveal(targets, options = {}) {
|
|
505
|
+
const {
|
|
506
|
+
effect = 'fade',
|
|
507
|
+
duration = 500,
|
|
508
|
+
delay = 0,
|
|
509
|
+
easing = 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
510
|
+
threshold = 0.15,
|
|
511
|
+
once = true,
|
|
512
|
+
distance = '24px',
|
|
513
|
+
} = options;
|
|
514
|
+
|
|
515
|
+
const els = typeof targets === 'string'
|
|
516
|
+
? Array.from(document.querySelectorAll(targets))
|
|
517
|
+
: targets instanceof Element ? [targets] : Array.from(targets);
|
|
518
|
+
|
|
519
|
+
if (!els.length) { console.warn('[scroll-snap-kit] scrollReveal: no elements found'); return () => { }; }
|
|
520
|
+
|
|
521
|
+
const hiddenStyles = {
|
|
522
|
+
'fade': { opacity: '0' },
|
|
523
|
+
'slide-up': { opacity: '0', transform: `translateY(${distance})` },
|
|
524
|
+
'slide-down': { opacity: '0', transform: `translateY(-${distance})` },
|
|
525
|
+
'slide-left': { opacity: '0', transform: `translateX(${distance})` },
|
|
526
|
+
'slide-right': { opacity: '0', transform: `translateX(-${distance})` },
|
|
527
|
+
'scale': { opacity: '0', transform: 'scale(0.9)' },
|
|
528
|
+
'fade-scale': { opacity: '0', transform: `scale(0.95) translateY(${distance})` },
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const hidden = hiddenStyles[effect] || hiddenStyles['fade'];
|
|
532
|
+
|
|
533
|
+
// Save original styles and apply hidden state
|
|
534
|
+
els.forEach((el, i) => {
|
|
535
|
+
el._ssk_origin = { transition: el.style.transition, ...Object.fromEntries(Object.keys(hidden).map(k => [k, el.style[k]])) };
|
|
536
|
+
Object.assign(el.style, hidden);
|
|
537
|
+
el.style.transition = `opacity ${duration}ms ${easing} ${delay + i * 0}ms, transform ${duration}ms ${easing} ${delay}ms`;
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const obs = new IntersectionObserver((entries) => {
|
|
541
|
+
entries.forEach(entry => {
|
|
542
|
+
if (entry.isIntersecting) {
|
|
543
|
+
const el = entry.target;
|
|
544
|
+
const i = els.indexOf(el);
|
|
545
|
+
setTimeout(() => {
|
|
546
|
+
el.style.opacity = '';
|
|
547
|
+
el.style.transform = '';
|
|
548
|
+
}, i * (delay || 0));
|
|
549
|
+
if (once) obs.unobserve(el);
|
|
550
|
+
} else if (!once) {
|
|
551
|
+
Object.assign(entry.target.style, hidden);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
}, { threshold });
|
|
555
|
+
|
|
556
|
+
els.forEach(el => obs.observe(el));
|
|
557
|
+
|
|
558
|
+
return () => {
|
|
559
|
+
obs.disconnect();
|
|
560
|
+
els.forEach(el => {
|
|
561
|
+
if (el._ssk_origin) {
|
|
562
|
+
el.style.transition = el._ssk_origin.transition;
|
|
563
|
+
el.style.opacity = el._ssk_origin.opacity || '';
|
|
564
|
+
el.style.transform = el._ssk_origin.transform || '';
|
|
565
|
+
delete el._ssk_origin;
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* scrollTimeline — drive CSS custom properties (variables) from scroll position,
|
|
573
|
+
* letting you animate anything via CSS using `var(--scroll-progress)` etc.
|
|
574
|
+
* Also supports directly animating numeric CSS properties on elements.
|
|
575
|
+
*
|
|
576
|
+
* @param {Array<{
|
|
577
|
+
* property: string, // CSS custom property name e.g. '--hero-opacity'
|
|
578
|
+
* from: number, // value at scrollStart
|
|
579
|
+
* to: number, // value at scrollEnd
|
|
580
|
+
* unit?: string, // CSS unit e.g. 'px', '%', 'deg', 'rem' (default: '')
|
|
581
|
+
* scrollStart?: number, // page scroll Y to begin (default: 0)
|
|
582
|
+
* scrollEnd?: number, // page scroll Y to end (default: document height)
|
|
583
|
+
* target?: Element|string, // element to set the property on (default: document.documentElement)
|
|
584
|
+
* }>} tracks
|
|
585
|
+
* @returns {() => void} cleanup function
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* const stop = scrollTimeline([
|
|
589
|
+
* { property: '--hero-opacity', from: 1, to: 0, scrollStart: 0, scrollEnd: 400 },
|
|
590
|
+
* { property: '--nav-blur', from: 0, to: 16, unit: 'px', scrollStart: 0, scrollEnd: 200 },
|
|
591
|
+
* ])
|
|
592
|
+
*/
|
|
593
|
+
function scrollTimeline(tracks, options = {}) {
|
|
594
|
+
if (!Array.isArray(tracks) || !tracks.length) {
|
|
595
|
+
console.warn('[scroll-snap-kit] scrollTimeline: tracks must be a non-empty array');
|
|
596
|
+
return () => { };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const resolved = tracks.map(t => ({
|
|
600
|
+
...t,
|
|
601
|
+
unit: t.unit ?? '',
|
|
602
|
+
scrollStart: t.scrollStart ?? 0,
|
|
603
|
+
scrollEnd: t.scrollEnd ?? (document.body.scrollHeight - window.innerHeight),
|
|
604
|
+
target: typeof t.target === 'string'
|
|
605
|
+
? document.querySelector(t.target)
|
|
606
|
+
: (t.target ?? document.documentElement),
|
|
607
|
+
}));
|
|
608
|
+
|
|
609
|
+
let rafId = null;
|
|
610
|
+
|
|
611
|
+
function update() {
|
|
612
|
+
const scrollY = window.scrollY;
|
|
613
|
+
resolved.forEach(({ property, from, to, unit, scrollStart, scrollEnd, target }) => {
|
|
614
|
+
const range = scrollEnd - scrollStart;
|
|
615
|
+
const progress = range <= 0 ? 1 : Math.min(1, Math.max(0, (scrollY - scrollStart) / range));
|
|
616
|
+
const value = from + (to - from) * progress;
|
|
617
|
+
target.style.setProperty(property, `${value}${unit}`);
|
|
618
|
+
});
|
|
619
|
+
rafId = null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const handler = () => { if (!rafId) rafId = requestAnimationFrame(update); };
|
|
623
|
+
window.addEventListener('scroll', handler, { passive: true });
|
|
624
|
+
update();
|
|
625
|
+
|
|
626
|
+
return () => {
|
|
627
|
+
window.removeEventListener('scroll', handler);
|
|
628
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
629
|
+
resolved.forEach(({ property, target }) => target.style.removeProperty(property));
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* infiniteScroll — fire a callback when the user scrolls near the bottom of the
|
|
635
|
+
* page (or a scrollable container), typically used to load more content.
|
|
636
|
+
*
|
|
637
|
+
* Automatically re-arms itself after the callback resolves (if it returns a Promise)
|
|
638
|
+
* or after a configurable cooldown, so rapid triggers are prevented.
|
|
639
|
+
*
|
|
640
|
+
* @param {() => void | Promise<void>} callback
|
|
641
|
+
* @param {{ threshold?: number, cooldown?: number, container?: Element }} options
|
|
642
|
+
* @returns {() => void} cleanup / stop function
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* const stop = infiniteScroll(async () => {
|
|
646
|
+
* const items = await fetchMoreItems()
|
|
647
|
+
* appendItems(items)
|
|
648
|
+
* }, { threshold: 300 })
|
|
649
|
+
*/
|
|
650
|
+
function infiniteScroll(callback, options = {}) {
|
|
651
|
+
const { threshold = 200, cooldown = 500, container } = options;
|
|
652
|
+
let loading = false;
|
|
653
|
+
|
|
654
|
+
const getRemaining = () => {
|
|
655
|
+
if (container) {
|
|
656
|
+
return container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
657
|
+
}
|
|
658
|
+
return document.body.scrollHeight - window.scrollY - window.innerHeight;
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const handler = async () => {
|
|
662
|
+
if (loading) return;
|
|
663
|
+
if (getRemaining() <= threshold) {
|
|
664
|
+
loading = true;
|
|
665
|
+
try {
|
|
666
|
+
await Promise.resolve(callback());
|
|
667
|
+
} finally {
|
|
668
|
+
setTimeout(() => { loading = false; }, cooldown);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const target = container || window;
|
|
674
|
+
target.addEventListener('scroll', handler, { passive: true });
|
|
675
|
+
handler(); // check immediately in case already near bottom
|
|
676
|
+
|
|
677
|
+
return () => target.removeEventListener('scroll', handler);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* scrollTrap — contain scroll within a specific element (like a modal or drawer),
|
|
682
|
+
* preventing the background page from scrolling while the element is open.
|
|
683
|
+
* Handles mouse wheel, touch, and keyboard arrow/space/pageup/pagedown events.
|
|
684
|
+
*
|
|
685
|
+
* Returns a release function.
|
|
686
|
+
*
|
|
687
|
+
* @param {Element | string} element
|
|
688
|
+
* @param {{ allowKeys?: boolean }} options
|
|
689
|
+
* @returns {() => void} release function
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* const release = scrollTrap(document.querySelector('.modal'))
|
|
693
|
+
* // …later, when modal closes:
|
|
694
|
+
* release()
|
|
695
|
+
*/
|
|
696
|
+
function scrollTrap(element, options = {}) {
|
|
697
|
+
const el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
698
|
+
if (!(el instanceof Element)) {
|
|
699
|
+
console.warn('[scroll-snap-kit] scrollTrap: element not found');
|
|
700
|
+
return () => { };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const { allowKeys = true } = options;
|
|
704
|
+
|
|
705
|
+
const canScrollUp = () => el.scrollTop > 0;
|
|
706
|
+
const canScrollDown = () => el.scrollTop < el.scrollHeight - el.clientHeight;
|
|
707
|
+
|
|
708
|
+
// Wheel handler — block wheel events that would escape the element
|
|
709
|
+
const onWheel = (e) => {
|
|
710
|
+
const goingDown = e.deltaY > 0;
|
|
711
|
+
if (goingDown && !canScrollDown()) { e.preventDefault(); return; }
|
|
712
|
+
if (!goingDown && !canScrollUp()) { e.preventDefault(); return; }
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// Touch handler
|
|
716
|
+
let touchStartY = 0;
|
|
717
|
+
const onTouchStart = (e) => { touchStartY = e.touches[0].clientY; };
|
|
718
|
+
const onTouchMove = (e) => {
|
|
719
|
+
const dy = touchStartY - e.touches[0].clientY;
|
|
720
|
+
if (dy > 0 && !canScrollDown()) { e.preventDefault(); return; }
|
|
721
|
+
if (dy < 0 && !canScrollUp()) { e.preventDefault(); return; }
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// Key handler
|
|
725
|
+
const SCROLL_KEYS = { ArrowUp: -40, ArrowDown: 40, PageUp: -300, PageDown: 300, ' ': 300 };
|
|
726
|
+
const onKeyDown = (e) => {
|
|
727
|
+
const delta = SCROLL_KEYS[e.key];
|
|
728
|
+
if (!delta) return;
|
|
729
|
+
if (!el.contains(document.activeElement) && document.activeElement !== el) return;
|
|
730
|
+
e.preventDefault();
|
|
731
|
+
el.scrollTop += delta;
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
el.addEventListener('wheel', onWheel, { passive: false });
|
|
735
|
+
el.addEventListener('touchstart', onTouchStart, { passive: true });
|
|
736
|
+
el.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
737
|
+
if (allowKeys) document.addEventListener('keydown', onKeyDown);
|
|
738
|
+
|
|
739
|
+
// Also lock the body
|
|
740
|
+
lockScroll();
|
|
741
|
+
|
|
742
|
+
return () => {
|
|
743
|
+
el.removeEventListener('wheel', onWheel);
|
|
744
|
+
el.removeEventListener('touchstart', onTouchStart);
|
|
745
|
+
el.removeEventListener('touchmove', onTouchMove);
|
|
746
|
+
if (allowKeys) document.removeEventListener('keydown', onKeyDown);
|
|
747
|
+
unlockScroll();
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* scroll-snap-kit — React Hooks
|
|
753
|
+
* Requires React 16.8+
|
|
754
|
+
*/
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Returns the current scroll position, updated on scroll.
|
|
758
|
+
* @param {{ throttle?: number, container?: Element }} options
|
|
759
|
+
* @returns {{ x: number, y: number, percentX: number, percentY: number }}
|
|
760
|
+
*/
|
|
761
|
+
function useScrollPosition(options = {}) {
|
|
762
|
+
const { throttle = 100, container } = options;
|
|
763
|
+
const [position, setPosition] = useState(() =>
|
|
764
|
+
typeof window !== 'undefined' ? getScrollPosition(container) : { x: 0, y: 0, percentX: 0, percentY: 0 }
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
useEffect(() => {
|
|
768
|
+
const cleanup = onScroll((pos) => setPosition(pos), { throttle, container });
|
|
769
|
+
return cleanup;
|
|
770
|
+
}, [throttle, container]);
|
|
771
|
+
|
|
772
|
+
return position;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Returns whether a referenced element is currently in the viewport.
|
|
777
|
+
* @param {{ threshold?: number, once?: boolean }} options
|
|
778
|
+
* @returns {[React.RefObject, boolean]}
|
|
779
|
+
*/
|
|
780
|
+
function useInViewport(options = {}) {
|
|
781
|
+
const { threshold = 0, once = false } = options;
|
|
782
|
+
const ref = useRef(null);
|
|
783
|
+
const [inView, setInView] = useState(false);
|
|
784
|
+
const hasTriggered = useRef(false);
|
|
785
|
+
|
|
786
|
+
useEffect(() => {
|
|
787
|
+
if (!ref.current) return;
|
|
788
|
+
|
|
789
|
+
const observer = new IntersectionObserver(
|
|
790
|
+
([entry]) => {
|
|
791
|
+
const visible = entry.isIntersecting;
|
|
792
|
+
if (once) {
|
|
793
|
+
if (visible && !hasTriggered.current) {
|
|
794
|
+
hasTriggered.current = true;
|
|
795
|
+
setInView(true);
|
|
796
|
+
observer.disconnect();
|
|
797
|
+
}
|
|
798
|
+
} else {
|
|
799
|
+
setInView(visible);
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
{ threshold }
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
observer.observe(ref.current);
|
|
806
|
+
return () => observer.disconnect();
|
|
807
|
+
}, [threshold, once]);
|
|
808
|
+
|
|
809
|
+
return [ref, inView];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Returns a scrollTo function scoped to an element ref or the window.
|
|
814
|
+
* @returns {[React.RefObject, (target: Element|number, options?: object) => void]}
|
|
815
|
+
*/
|
|
816
|
+
function useScrollTo() {
|
|
817
|
+
const containerRef = useRef(null);
|
|
818
|
+
|
|
819
|
+
const scrollToTarget = useCallback((target, options = {}) => {
|
|
820
|
+
const { behavior = 'smooth', offset = 0 } = options;
|
|
821
|
+
const container = containerRef.current;
|
|
822
|
+
|
|
823
|
+
if (!container) {
|
|
824
|
+
// fallback to window scroll
|
|
825
|
+
if (typeof target === 'number') {
|
|
826
|
+
window.scrollTo({ top: target + offset, behavior });
|
|
827
|
+
} else if (target instanceof Element) {
|
|
828
|
+
target.scrollIntoView({ behavior });
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (typeof target === 'number') {
|
|
834
|
+
container.scrollTo({ top: target + offset, behavior });
|
|
835
|
+
} else if (target instanceof Element) {
|
|
836
|
+
const containerTop = container.getBoundingClientRect().top;
|
|
837
|
+
const targetTop = target.getBoundingClientRect().top;
|
|
838
|
+
container.scrollBy({ top: targetTop - containerTop + offset, behavior });
|
|
839
|
+
}
|
|
840
|
+
}, []);
|
|
841
|
+
|
|
842
|
+
return [containerRef, scrollToTarget];
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Tracks whether the user has scrolled past a given pixel threshold.
|
|
847
|
+
* @param {number} threshold - Y pixel value to check against (default: 100)
|
|
848
|
+
* @param {{ container?: Element }} options
|
|
849
|
+
* @returns {boolean}
|
|
850
|
+
*/
|
|
851
|
+
function useScrolledPast(threshold = 100, options = {}) {
|
|
852
|
+
const { container } = options;
|
|
853
|
+
const [scrolledPast, setScrolledPast] = useState(false);
|
|
854
|
+
|
|
855
|
+
useEffect(() => {
|
|
856
|
+
const cleanup = onScroll(({ y }) => {
|
|
857
|
+
setScrolledPast(y > threshold);
|
|
858
|
+
}, { container });
|
|
859
|
+
return cleanup;
|
|
860
|
+
}, [threshold, container]);
|
|
861
|
+
|
|
862
|
+
return scrolledPast;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Returns scroll direction: 'up' | 'down' | null
|
|
867
|
+
* @param {{ throttle?: number }} options
|
|
868
|
+
* @returns {'up'|'down'|null}
|
|
869
|
+
*/
|
|
870
|
+
function useScrollDirection(options = {}) {
|
|
871
|
+
const { throttle = 100 } = options;
|
|
872
|
+
const [direction, setDirection] = useState(null);
|
|
873
|
+
const lastY = useRef(typeof window !== 'undefined' ? window.scrollY : 0);
|
|
874
|
+
|
|
875
|
+
useEffect(() => {
|
|
876
|
+
const cleanup = onScroll(({ y }) => {
|
|
877
|
+
setDirection(y > lastY.current ? 'down' : 'up');
|
|
878
|
+
lastY.current = y;
|
|
879
|
+
}, { throttle });
|
|
880
|
+
return cleanup;
|
|
881
|
+
}, [throttle]);
|
|
882
|
+
|
|
883
|
+
return direction;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
module.exports = {
|
|
887
|
+
scrollTo,
|
|
888
|
+
scrollToTop,
|
|
889
|
+
scrollToBottom,
|
|
890
|
+
getScrollPosition,
|
|
891
|
+
onScroll,
|
|
892
|
+
isInViewport,
|
|
893
|
+
lockScroll,
|
|
894
|
+
unlockScroll,
|
|
895
|
+
scrollSpy,
|
|
896
|
+
onScrollEnd,
|
|
897
|
+
scrollIntoViewIfNeeded,
|
|
898
|
+
Easings,
|
|
899
|
+
easeScroll,
|
|
900
|
+
scrollSequence,
|
|
901
|
+
parallax,
|
|
902
|
+
scrollProgress,
|
|
903
|
+
snapToSection,
|
|
904
|
+
scrollReveal,
|
|
905
|
+
scrollTimeline,
|
|
906
|
+
infiniteScroll,
|
|
907
|
+
scrollTrap,
|
|
908
|
+
useScrollPosition,
|
|
909
|
+
useInViewport,
|
|
910
|
+
useScrollTo,
|
|
911
|
+
useScrolledPast,
|
|
912
|
+
useScrollDirection,
|
|
913
|
+
};
|