source: Klonkt/src/assets/vendor/lenis.mjs@ 88838bc

main
Last change on this file since 88838bc was 88838bc, checked in by Robin <roboburr@…>, 3 weeks ago

Opruimen rond Lenis: een kopie, de juiste map, en de licenties genoteerd

Robins vraag of er iets met de MIT-licentie moest, en het antwoord bleek
groter dan de vraag.

LENIS ZAT ER AL. src/assets/js/lenis.min.js (1.1.13) plus een
smooth-scroll.js die er een instantie mee maakte. Ik heb daar gisteren een
tweede kopie naast gezet (1.3.26, ESM) zonder te kijken -- twee versies van
dezelfde bibliotheek in twee mappen. Had ik moeten opzoeken voor ik ging
vendoren.

Het gevaar dat ik vreesde was er niet: smooth-scroll.js wordt door geen
enkel sjabloon ingeladen, dus er draaiden geen twee instanties. Het was dode
code. Wel leerzaam: die oude opzet stond op duration 1.0 met easeOutExpo,
precies de trage instelling die Robin gisteren als "te zweverig" herkende.

Opgeruimd:

  • lenis.min.js (1.1.13) en smooth-scroll.js weg -- ongebruikt.
  • Mijn ESM-kopie verhuisd van src/assets/js/vendor/ naar src/assets/vendor/, waar cropper en simplewebauthn al stonden. Een tweede vendor-map naast de bestaande was mijn eigen verzinsel.
  • data-lenis-prevent in topnav.ejs blijft staan: dat werkt weer, want er draait nu daadwerkelijk een Lenis. Het hoorde bij de oude poging en is nu toevallig weer zinvol -- vandaar deze regel, zodat de volgende lezer niet denkt dat het een restant is.

LICENTIES. MIT eist dat de copyrightvermelding meereist met de code, en die
staat in src/assets/vendor/lenis-LICENSE.txt. Maar de repo heeft een
THIRD-PARTY-NOTICES.md waar de README naar verwijst, en daar ontbrak een
sectie voor gevendorde browser-bibliotheken -- niet alleen Lenis, ook
cropper, simplewebauthn en de losse htmx-kopie stonden er niet in. Die
sectie is er nu, met versie en licentie per bestand.

Alles nagekeken in de bestanden zelf en via npm view, niet uit het hoofd:
Lenis 1.3.26 MIT, Cropper.js 1.6.2 MIT, @simplewebauthn/browser 13.3.0 MIT,
htmx 0BSD. Klonkt is AGPL-3.0-or-later en MIT-code mag daarin.

MOD_V naar 20 (het importpad in read.js is verhuisd).

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 32.4 KB
Line 
1//#region package.json
2var version = "1.3.26";
3//#endregion
4//#region packages/core/src/maths.ts
5/**
6* Clamp a value between a minimum and maximum value
7*
8* @param min Minimum value
9* @param input Value to clamp
10* @param max Maximum value
11* @returns Clamped value
12*/
13function clamp(min, input, max) {
14 return Math.max(min, Math.min(input, max));
15}
16/**
17* Linearly interpolate between two values using an amount (0 <= t <= 1)
18*
19* @param x First value
20* @param y Second value
21* @param t Amount to interpolate (0 <= t <= 1)
22* @returns Interpolated value
23*/
24function lerp(x, y, t) {
25 return (1 - t) * x + t * y;
26}
27/**
28* Damp a value over time using a damping factor
29* {@link http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/}
30*
31* @param x Initial value
32* @param y Target value
33* @param lambda Damping factor
34* @param dt Time elapsed since the last update
35* @returns Damped value
36*/
37function damp(x, y, lambda, deltaTime) {
38 return lerp(x, y, 1 - Math.exp(-lambda * deltaTime));
39}
40/**
41* Calculate the modulo of the dividend and divisor while keeping the result within the same sign as the divisor
42* {@link https://anguscroll.com/just/just-modulo}
43*
44* @param n Dividend
45* @param d Divisor
46* @returns Modulo
47*/
48function modulo(n, d) {
49 return (n % d + d) % d;
50}
51//#endregion
52//#region packages/core/src/animate.ts
53/**
54* Animate class to handle value animations with lerping or easing
55*
56* @example
57* const animate = new Animate()
58* animate.fromTo(0, 100, { duration: 1, easing: (t) => t })
59* animate.advance(0.5) // 50
60*/
61var Animate = class {
62 isRunning = false;
63 value = 0;
64 from = 0;
65 to = 0;
66 currentTime = 0;
67 lerp;
68 duration;
69 easing;
70 onUpdate;
71 /**
72 * Advance the animation by the given delta time
73 *
74 * @param deltaTime - The time in seconds to advance the animation
75 */
76 advance(deltaTime) {
77 if (!this.isRunning) return;
78 let completed = false;
79 if (this.duration && this.easing) {
80 this.currentTime += deltaTime;
81 const linearProgress = clamp(0, this.currentTime / this.duration, 1);
82 completed = linearProgress >= 1;
83 const easedProgress = completed ? 1 : this.easing(linearProgress);
84 this.value = this.from + (this.to - this.from) * easedProgress;
85 } else if (this.lerp) {
86 this.value = damp(this.value, this.to, this.lerp * 60, deltaTime);
87 if (Math.round(this.value) === Math.round(this.to)) {
88 this.value = this.to;
89 completed = true;
90 }
91 } else {
92 this.value = this.to;
93 completed = true;
94 }
95 if (completed) this.stop();
96 this.onUpdate?.(this.value, completed);
97 }
98 /** Stop the animation */
99 stop() {
100 this.isRunning = false;
101 }
102 /**
103 * Set up the animation from a starting value to an ending value
104 * with optional parameters for lerping, duration, easing, and onUpdate callback
105 *
106 * @param from - The starting value
107 * @param to - The ending value
108 * @param options - Options for the animation
109 */
110 fromTo(from, to, { lerp, duration, easing, onStart, onUpdate }) {
111 this.from = this.value = from;
112 this.to = to;
113 this.lerp = lerp;
114 this.duration = duration;
115 this.easing = easing;
116 this.currentTime = 0;
117 this.isRunning = true;
118 onStart?.();
119 this.onUpdate = onUpdate;
120 }
121};
122//#endregion
123//#region packages/core/src/debounce.ts
124function debounce(callback, delay) {
125 let timer;
126 return function(...args) {
127 clearTimeout(timer);
128 timer = setTimeout(() => {
129 timer = void 0;
130 callback.apply(this, args);
131 }, delay);
132 };
133}
134//#endregion
135//#region packages/core/src/dimensions.ts
136/**
137* Dimensions class to handle the size of the content and wrapper
138*
139* @example
140* const dimensions = new Dimensions(wrapper, content)
141* dimensions.on('resize', (e) => {
142* console.log(e.width, e.height)
143* })
144*/
145var Dimensions = class {
146 width = 0;
147 height = 0;
148 scrollHeight = 0;
149 scrollWidth = 0;
150 debouncedResize;
151 wrapperResizeObserver;
152 contentResizeObserver;
153 constructor(wrapper, content, { autoResize = true, debounce: debounceValue = 250 } = {}) {
154 this.wrapper = wrapper;
155 this.content = content;
156 if (autoResize) {
157 this.debouncedResize = debounce(this.resize, debounceValue);
158 if (this.wrapper instanceof Window) window.addEventListener("resize", this.debouncedResize);
159 else {
160 this.wrapperResizeObserver = new ResizeObserver(this.debouncedResize);
161 this.wrapperResizeObserver.observe(this.wrapper);
162 }
163 this.contentResizeObserver = new ResizeObserver(this.debouncedResize);
164 this.contentResizeObserver.observe(this.content);
165 }
166 this.resize();
167 }
168 destroy() {
169 this.wrapperResizeObserver?.disconnect();
170 this.contentResizeObserver?.disconnect();
171 if (this.wrapper === window && this.debouncedResize) window.removeEventListener("resize", this.debouncedResize);
172 }
173 resize = () => {
174 this.onWrapperResize();
175 this.onContentResize();
176 };
177 onWrapperResize = () => {
178 if (this.wrapper instanceof Window) {
179 this.width = window.innerWidth;
180 this.height = window.innerHeight;
181 } else {
182 this.width = this.wrapper.clientWidth;
183 this.height = this.wrapper.clientHeight;
184 }
185 };
186 onContentResize = () => {
187 if (this.wrapper instanceof Window) {
188 this.scrollHeight = this.content.scrollHeight;
189 this.scrollWidth = this.content.scrollWidth;
190 } else {
191 this.scrollHeight = this.wrapper.scrollHeight;
192 this.scrollWidth = this.wrapper.scrollWidth;
193 }
194 };
195 get limit() {
196 return {
197 x: this.scrollWidth - this.width,
198 y: this.scrollHeight - this.height
199 };
200 }
201};
202//#endregion
203//#region packages/core/src/emitter.ts
204/**
205* Emitter class to handle events
206* @example
207* const emitter = new Emitter()
208* emitter.on('event', (data) => {
209* console.log(data)
210* })
211* emitter.emit('event', 'data')
212*/
213var Emitter = class {
214 events = {};
215 /**
216 * Emit an event with the given data
217 * @param event Event name
218 * @param args Data to pass to the event handlers
219 */
220 emit(event, ...args) {
221 const callbacks = this.events[event] || [];
222 for (let i = 0, length = callbacks.length; i < length; i++) callbacks[i]?.(...args);
223 }
224 /**
225 * Add a callback to the event
226 * @param event Event name
227 * @param cb Callback function
228 * @returns Unsubscribe function
229 */
230 on(event, cb) {
231 if (this.events[event]) this.events[event].push(cb);
232 else this.events[event] = [cb];
233 return () => {
234 this.events[event] = this.events[event]?.filter((i) => cb !== i);
235 };
236 }
237 /**
238 * Remove a callback from the event
239 * @param event Event name
240 * @param callback Callback function
241 */
242 off(event, callback) {
243 this.events[event] = this.events[event]?.filter((i) => callback !== i);
244 }
245 /**
246 * Remove all event listeners and clean up
247 */
248 destroy() {
249 this.events = {};
250 }
251};
252//#endregion
253//#region packages/core/src/virtual-scroll.ts
254const LINE_HEIGHT = 100 / 6;
255const listenerOptions = { passive: false };
256function getDeltaMultiplier(deltaMode, size) {
257 if (deltaMode === 1) return LINE_HEIGHT;
258 if (deltaMode === 2) return size;
259 return 1;
260}
261var VirtualScroll = class {
262 touchStart = {
263 x: 0,
264 y: 0
265 };
266 lastDelta = {
267 x: 0,
268 y: 0
269 };
270 window = {
271 width: 0,
272 height: 0
273 };
274 emitter = new Emitter();
275 constructor(element, options = {
276 wheelMultiplier: 1,
277 touchMultiplier: 1
278 }) {
279 this.element = element;
280 this.options = options;
281 window.addEventListener("resize", this.onWindowResize);
282 this.onWindowResize();
283 this.element.addEventListener("wheel", this.onWheel, listenerOptions);
284 this.element.addEventListener("touchstart", this.onTouchStart, listenerOptions);
285 this.element.addEventListener("touchmove", this.onTouchMove, listenerOptions);
286 this.element.addEventListener("touchend", this.onTouchEnd, listenerOptions);
287 }
288 /**
289 * Add an event listener for the given event and callback
290 *
291 * @param event Event name
292 * @param callback Callback function
293 */
294 on(event, callback) {
295 return this.emitter.on(event, callback);
296 }
297 /** Remove all event listeners and clean up */
298 destroy() {
299 this.emitter.destroy();
300 window.removeEventListener("resize", this.onWindowResize);
301 this.element.removeEventListener("wheel", this.onWheel, listenerOptions);
302 this.element.removeEventListener("touchstart", this.onTouchStart, listenerOptions);
303 this.element.removeEventListener("touchmove", this.onTouchMove, listenerOptions);
304 this.element.removeEventListener("touchend", this.onTouchEnd, listenerOptions);
305 }
306 /**
307 * Event handler for 'touchstart' event
308 *
309 * @param event Touch event
310 */
311 onTouchStart = (event) => {
312 const { clientX, clientY } = event.targetTouches ? event.targetTouches[0] : event;
313 this.touchStart.x = clientX;
314 this.touchStart.y = clientY;
315 this.lastDelta = {
316 x: 0,
317 y: 0
318 };
319 this.emitter.emit("scroll", {
320 deltaX: 0,
321 deltaY: 0,
322 event
323 });
324 };
325 /** Event handler for 'touchmove' event */
326 onTouchMove = (event) => {
327 const { clientX, clientY } = event.targetTouches ? event.targetTouches[0] : event;
328 const deltaX = -(clientX - this.touchStart.x) * this.options.touchMultiplier;
329 const deltaY = -(clientY - this.touchStart.y) * this.options.touchMultiplier;
330 this.touchStart.x = clientX;
331 this.touchStart.y = clientY;
332 this.lastDelta = {
333 x: deltaX,
334 y: deltaY
335 };
336 this.emitter.emit("scroll", {
337 deltaX,
338 deltaY,
339 event
340 });
341 };
342 onTouchEnd = (event) => {
343 this.emitter.emit("scroll", {
344 deltaX: this.lastDelta.x,
345 deltaY: this.lastDelta.y,
346 event
347 });
348 };
349 /** Event handler for 'wheel' event */
350 onWheel = (event) => {
351 let { deltaX, deltaY, deltaMode } = event;
352 const multiplierX = getDeltaMultiplier(deltaMode, this.window.width);
353 const multiplierY = getDeltaMultiplier(deltaMode, this.window.height);
354 deltaX *= multiplierX;
355 deltaY *= multiplierY;
356 deltaX *= this.options.wheelMultiplier;
357 deltaY *= this.options.wheelMultiplier;
358 this.emitter.emit("scroll", {
359 deltaX,
360 deltaY,
361 event
362 });
363 };
364 onWindowResize = () => {
365 this.window = {
366 width: window.innerWidth,
367 height: window.innerHeight
368 };
369 };
370};
371//#endregion
372//#region packages/core/src/lenis.ts
373const defaultEasing = (t) => Math.min(1, 1.001 - 2 ** (-10 * t));
374var Lenis = class {
375 _isScrolling = false;
376 _isStopped = false;
377 _isLocked = false;
378 _preventNextNativeScrollEvent = false;
379 _resetVelocityTimeout = null;
380 _rafId = null;
381 _isDraggingSelection = false;
382 reducedMotionMediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
383 /**
384 * Whether or not the user is touching the screen
385 */
386 isTouching;
387 /**
388 * Whether or not the device is running iOS
389 */
390 isIos;
391 /**
392 * The time in ms since the lenis instance was created
393 */
394 time = 0;
395 /**
396 * User data that will be forwarded through the scroll event
397 *
398 * @example
399 * lenis.scrollTo(100, {
400 * userData: {
401 * foo: 'bar'
402 * }
403 * })
404 */
405 userData = {};
406 /**
407 * The last velocity of the scroll
408 */
409 lastVelocity = 0;
410 /**
411 * The current velocity of the scroll
412 */
413 velocity = 0;
414 /**
415 * The direction of the scroll
416 */
417 direction = 0;
418 /**
419 * The options passed to the lenis instance
420 */
421 options;
422 /**
423 * The target scroll value
424 */
425 targetScroll;
426 /**
427 * The animated scroll value
428 */
429 animatedScroll;
430 animate = new Animate();
431 emitter = new Emitter();
432 dimensions;
433 virtualScroll;
434 constructor({ wrapper = window, content = document.documentElement, eventsTarget = wrapper, smoothWheel = true, syncTouch = false, syncTouchLerp = .075, touchInertiaExponent = 1.7, duration, easing, lerp = .1, infinite = false, orientation = "vertical", gestureOrientation = orientation === "horizontal" ? "both" : "vertical", touchMultiplier = 1, wheelMultiplier = 1, autoResize = true, prevent, virtualScroll, overscroll = true, autoRaf = false, anchors = false, autoToggle = false, allowNestedScroll = false, __experimental__naiveDimensions = false, naiveDimensions = __experimental__naiveDimensions, stopInertiaOnNavigate = false, respectReducedMotion = true } = {}) {
435 window.lenisVersion = version;
436 if (!window.lenis) window.lenis = {};
437 window.lenis.version = version;
438 if (orientation === "horizontal") window.lenis.horizontal = true;
439 if (syncTouch === true) window.lenis.touch = true;
440 this.isIos = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
441 if (!wrapper || wrapper === document.documentElement) wrapper = window;
442 if (typeof duration === "number" && typeof easing !== "function") easing = defaultEasing;
443 else if (typeof easing === "function" && typeof duration !== "number") duration = 1;
444 this.options = {
445 wrapper,
446 content,
447 eventsTarget,
448 smoothWheel,
449 syncTouch,
450 syncTouchLerp,
451 touchInertiaExponent,
452 duration,
453 easing,
454 lerp,
455 infinite,
456 gestureOrientation,
457 orientation,
458 touchMultiplier,
459 wheelMultiplier,
460 autoResize,
461 prevent,
462 virtualScroll,
463 overscroll,
464 autoRaf,
465 anchors,
466 autoToggle,
467 allowNestedScroll,
468 naiveDimensions,
469 stopInertiaOnNavigate,
470 respectReducedMotion
471 };
472 this.dimensions = new Dimensions(wrapper, content, { autoResize });
473 this.updateClassName();
474 this.targetScroll = this.animatedScroll = this.actualScroll;
475 this.options.wrapper.addEventListener("scroll", this.onNativeScroll);
476 this.options.wrapper.addEventListener("scrollend", this.onScrollEnd, { capture: true });
477 if (this.options.anchors || this.options.stopInertiaOnNavigate) this.options.wrapper.addEventListener("click", this.onClick);
478 this.options.wrapper.addEventListener("pointerdown", this.onPointerDown);
479 this.virtualScroll = new VirtualScroll(eventsTarget, {
480 touchMultiplier,
481 wheelMultiplier
482 });
483 this.virtualScroll.on("scroll", this.onVirtualScroll);
484 if (this.options.autoToggle) {
485 this.checkOverflow();
486 this.rootElement.addEventListener("transitionend", this.onTransitionEnd);
487 }
488 if (this.options.autoRaf) this._rafId = requestAnimationFrame(this.raf);
489 }
490 /**
491 * Destroy the lenis instance, remove all event listeners and clean up the class name
492 */
493 destroy() {
494 this.emitter.destroy();
495 this.options.wrapper.removeEventListener("scroll", this.onNativeScroll);
496 this.options.wrapper.removeEventListener("scrollend", this.onScrollEnd, { capture: true });
497 this.options.wrapper.removeEventListener("pointerdown", this.onPointerDown);
498 if (this.options.anchors || this.options.stopInertiaOnNavigate) this.options.wrapper.removeEventListener("click", this.onClick);
499 this.virtualScroll.destroy();
500 this.dimensions.destroy();
501 this.cleanUpClassName();
502 if (this._rafId) cancelAnimationFrame(this._rafId);
503 }
504 on(event, callback) {
505 return this.emitter.on(event, callback);
506 }
507 off(event, callback) {
508 return this.emitter.off(event, callback);
509 }
510 onScrollEnd = (e) => {
511 if (!(e instanceof CustomEvent)) {
512 if (this.isScrolling === "smooth" || this.isScrolling === false) e.stopPropagation();
513 }
514 };
515 dispatchScrollendEvent = () => {
516 this.options.wrapper.dispatchEvent(new CustomEvent("scrollend", {
517 bubbles: this.options.wrapper === window,
518 detail: { lenisScrollEnd: true }
519 }));
520 };
521 get overflow() {
522 const property = this.isHorizontal ? "overflow-x" : "overflow-y";
523 return getComputedStyle(this.rootElement)[property];
524 }
525 checkOverflow() {
526 if (["hidden", "clip"].includes(this.overflow)) this.internalStop();
527 else this.internalStart();
528 }
529 onTransitionEnd = (event) => {
530 if (event.propertyName?.includes("overflow") && event.target === this.rootElement) this.checkOverflow();
531 };
532 setScroll(scroll) {
533 if (this.isHorizontal) this.options.wrapper.scrollTo({
534 left: scroll,
535 behavior: "instant"
536 });
537 else this.options.wrapper.scrollTo({
538 top: scroll,
539 behavior: "instant"
540 });
541 }
542 onClick = (event) => {
543 const linkElementsUrls = event.composedPath().filter((node) => node instanceof HTMLAnchorElement && node.href).map((element) => new URL(element.href));
544 const currentUrl = new URL(window.location.href);
545 if (this.options.anchors) {
546 const anchorElementUrl = linkElementsUrls.find((targetUrl) => currentUrl.host === targetUrl.host && currentUrl.pathname === targetUrl.pathname && targetUrl.hash);
547 if (anchorElementUrl) {
548 const options = typeof this.options.anchors === "object" && this.options.anchors ? this.options.anchors : void 0;
549 const target = decodeURIComponent(anchorElementUrl.hash);
550 this.scrollTo(target, options);
551 return;
552 }
553 }
554 if (this.options.stopInertiaOnNavigate) {
555 if (linkElementsUrls.some((targetUrl) => currentUrl.host === targetUrl.host && currentUrl.pathname !== targetUrl.pathname)) {
556 this.reset();
557 return;
558 }
559 }
560 };
561 onPointerDown = (event) => {
562 if (event.button === 1) this.reset();
563 };
564 isTouchOnSelectionHandle(event) {
565 const selection = window.getSelection();
566 if (!selection || selection.isCollapsed || selection.rangeCount === 0) return false;
567 const touch = event.targetTouches[0] ?? event.changedTouches[0];
568 if (!touch) return false;
569 const rects = selection.getRangeAt(0).getClientRects();
570 if (rects.length === 0) return false;
571 const first = rects[0];
572 const last = rects[rects.length - 1];
573 const HANDLE_RADIUS = 40;
574 const nearStart = Math.hypot(touch.clientX - first.left, touch.clientY - first.top) <= HANDLE_RADIUS;
575 const nearEnd = Math.hypot(touch.clientX - last.right, touch.clientY - last.bottom) <= HANDLE_RADIUS;
576 return nearStart || nearEnd;
577 }
578 onVirtualScroll = (data) => {
579 if (typeof this.options.virtualScroll === "function" && this.options.virtualScroll(data) === false) return;
580 const { deltaX, deltaY, event } = data;
581 this.emitter.emit("virtual-scroll", {
582 deltaX,
583 deltaY,
584 event
585 });
586 if (event.ctrlKey) return;
587 if (event.lenisStopPropagation) return;
588 const isTouch = event.type.includes("touch");
589 const isWheel = event.type.includes("wheel");
590 if (isTouch && this.isIos) {
591 if (event.type === "touchstart") this._isDraggingSelection = this.isTouchOnSelectionHandle(event);
592 if (this._isDraggingSelection) {
593 if (event.type === "touchend") this._isDraggingSelection = false;
594 return;
595 }
596 }
597 this.isTouching = event.type === "touchstart" || event.type === "touchmove";
598 const isClickOrTap = deltaX === 0 && deltaY === 0;
599 if (this.options.syncTouch && isTouch && event.type === "touchstart" && isClickOrTap && !this.isStopped && !this.isLocked) {
600 this.reset();
601 return;
602 }
603 const isUnknownGesture = this.options.gestureOrientation === "vertical" && deltaY === 0 || this.options.gestureOrientation === "horizontal" && deltaX === 0;
604 if (isClickOrTap || isUnknownGesture) return;
605 let composedPath = event.composedPath();
606 composedPath = composedPath.slice(0, composedPath.indexOf(this.rootElement));
607 const prevent = this.options.prevent;
608 const gestureOrientation = Math.abs(deltaX) >= Math.abs(deltaY) ? "horizontal" : "vertical";
609 if (composedPath.find((node) => node instanceof HTMLElement && (typeof prevent === "function" && prevent?.(node) || node.hasAttribute?.("data-lenis-prevent") || gestureOrientation === "vertical" && node.hasAttribute?.("data-lenis-prevent-vertical") || gestureOrientation === "horizontal" && node.hasAttribute?.("data-lenis-prevent-horizontal") || isTouch && node.hasAttribute?.("data-lenis-prevent-touch") || isWheel && node.hasAttribute?.("data-lenis-prevent-wheel") || this.options.allowNestedScroll && this.hasNestedScroll(node, {
610 deltaX,
611 deltaY
612 })))) return;
613 if (this.isStopped || this.isLocked) {
614 if (event.cancelable) event.preventDefault();
615 return;
616 }
617 if (!(this.options.syncTouch && isTouch || this.options.smoothWheel && isWheel)) {
618 this.isScrolling = "native";
619 this.animate.stop();
620 event.lenisStopPropagation = true;
621 return;
622 }
623 let delta = deltaY;
624 if (this.options.gestureOrientation === "both") delta = Math.abs(deltaY) > Math.abs(deltaX) ? deltaY : deltaX;
625 else if (this.options.gestureOrientation === "horizontal") delta = deltaX;
626 if (!this.options.overscroll || this.options.infinite || this.options.wrapper !== window && this.limit > 0 && (this.animatedScroll > 0 && this.animatedScroll < this.limit || this.animatedScroll === 0 && deltaY > 0 || this.animatedScroll === this.limit && deltaY < 0)) event.lenisStopPropagation = true;
627 if (event.cancelable) event.preventDefault();
628 const isSyncTouch = isTouch && this.options.syncTouch;
629 const hasTouchInertia = isTouch && event.type === "touchend";
630 if (hasTouchInertia) delta = Math.sign(delta) * Math.abs(this.velocity) ** this.options.touchInertiaExponent;
631 this.scrollTo(this.targetScroll + delta, {
632 programmatic: false,
633 ...isSyncTouch ? { lerp: hasTouchInertia ? this.options.syncTouchLerp : 1 } : {
634 lerp: this.options.lerp,
635 duration: this.options.duration,
636 easing: this.options.easing
637 }
638 });
639 };
640 /**
641 * Force lenis to recalculate the dimensions
642 */
643 resize() {
644 this.dimensions.resize();
645 this.animatedScroll = this.targetScroll = this.actualScroll;
646 this.emit();
647 }
648 emit() {
649 this.emitter.emit("scroll", this);
650 }
651 onNativeScroll = () => {
652 if (this._resetVelocityTimeout !== null) {
653 clearTimeout(this._resetVelocityTimeout);
654 this._resetVelocityTimeout = null;
655 }
656 if (this._preventNextNativeScrollEvent) {
657 this._preventNextNativeScrollEvent = false;
658 return;
659 }
660 if (this.isScrolling === false || this.isScrolling === "native") {
661 const lastScroll = this.animatedScroll;
662 this.animatedScroll = this.targetScroll = this.actualScroll;
663 this.lastVelocity = this.velocity;
664 this.velocity = this.animatedScroll - lastScroll;
665 this.direction = Math.sign(this.animatedScroll - lastScroll);
666 if (!this.isStopped) this.isScrolling = "native";
667 this.emit();
668 if (this.velocity !== 0) this._resetVelocityTimeout = setTimeout(() => {
669 this.lastVelocity = this.velocity;
670 this.velocity = 0;
671 this.isScrolling = false;
672 this.emit();
673 }, 400);
674 }
675 };
676 reset() {
677 this.isLocked = false;
678 this.isScrolling = false;
679 this.animatedScroll = this.targetScroll = this.actualScroll;
680 this.lastVelocity = this.velocity = 0;
681 this.animate.stop();
682 }
683 /**
684 * Start lenis scroll after it has been stopped
685 */
686 start() {
687 if (!this.isStopped) return;
688 if (this.options.autoToggle) {
689 this.rootElement.style.removeProperty("overflow");
690 return;
691 }
692 this.internalStart();
693 }
694 internalStart() {
695 if (!this.isStopped) return;
696 this.reset();
697 this.isStopped = false;
698 this.emit();
699 }
700 /**
701 * Stop lenis scroll
702 */
703 stop() {
704 if (this.isStopped) return;
705 if (this.options.autoToggle) {
706 this.rootElement.style.setProperty("overflow", "clip");
707 return;
708 }
709 this.internalStop();
710 }
711 internalStop() {
712 if (this.isStopped) return;
713 this.reset();
714 this.isStopped = true;
715 this.emit();
716 }
717 /**
718 * RequestAnimationFrame for lenis
719 *
720 * @param time The time in ms from an external clock like `requestAnimationFrame` or Tempus
721 */
722 raf = (time) => {
723 const deltaTime = time - (this.time || time);
724 this.time = time;
725 this.animate.advance(deltaTime * .001);
726 if (this.options.autoRaf) this._rafId = requestAnimationFrame(this.raf);
727 };
728 /**
729 * Scroll to a target value
730 *
731 * @param target The target value to scroll to
732 * @param options The options for the scroll
733 *
734 * @example
735 * lenis.scrollTo(100, {
736 * offset: 100,
737 * duration: 1,
738 * easing: (t) => 1 - Math.cos((t * Math.PI) / 2),
739 * lerp: 0.1,
740 * onStart: () => {
741 * console.log('onStart')
742 * },
743 * onComplete: () => {
744 * console.log('onComplete')
745 * },
746 * })
747 */
748 scrollTo(_target, { offset = 0, immediate = false, lock = false, programmatic = true, lerp = programmatic ? this.options.lerp : void 0, duration = programmatic ? this.options.duration : void 0, easing = programmatic ? this.options.easing : void 0, onStart, onComplete, force = false, userData } = {}) {
749 if (this.prefersReducedMotion) if (programmatic) immediate = true;
750 else {
751 lerp = 1;
752 duration = void 0;
753 easing = void 0;
754 }
755 if ((this.isStopped || this.isLocked) && !force) return;
756 let target = _target;
757 let adjustedOffset = offset;
758 if (typeof target === "string" && [
759 "top",
760 "left",
761 "start",
762 "#"
763 ].includes(target)) target = 0;
764 else if (typeof target === "string" && [
765 "bottom",
766 "right",
767 "end"
768 ].includes(target)) target = this.limit;
769 else {
770 let node = null;
771 if (typeof target === "string") {
772 node = target.startsWith("#") ? document.getElementById(target.slice(1)) : document.querySelector(target);
773 if (!node) if (target === "#top") target = 0;
774 else console.warn("Lenis: Target not found", target);
775 } else if (target instanceof HTMLElement && target?.nodeType) node = target;
776 if (node) {
777 if (this.options.wrapper !== window) {
778 const wrapperRect = this.rootElement.getBoundingClientRect();
779 adjustedOffset -= this.isHorizontal ? wrapperRect.left : wrapperRect.top;
780 }
781 const rect = node.getBoundingClientRect();
782 const targetStyle = getComputedStyle(node);
783 const scrollMargin = this.isHorizontal ? Number.parseFloat(targetStyle.scrollMarginLeft) : Number.parseFloat(targetStyle.scrollMarginTop);
784 const containerStyle = getComputedStyle(this.rootElement);
785 const scrollPadding = this.isHorizontal ? Number.parseFloat(containerStyle.scrollPaddingLeft) : Number.parseFloat(containerStyle.scrollPaddingTop);
786 target = (this.isHorizontal ? rect.left : rect.top) + this.animatedScroll - (Number.isNaN(scrollMargin) ? 0 : scrollMargin) - (Number.isNaN(scrollPadding) ? 0 : scrollPadding);
787 }
788 }
789 if (typeof target !== "number") return;
790 target += adjustedOffset;
791 if (this.options.infinite) {
792 if (programmatic) {
793 this.targetScroll = this.animatedScroll = this.scroll;
794 const distance = target - this.animatedScroll;
795 if (distance > this.limit / 2) target -= this.limit;
796 else if (distance < -this.limit / 2) target += this.limit;
797 }
798 } else target = clamp(0, target, this.limit);
799 if (target === this.targetScroll) {
800 onStart?.(this);
801 onComplete?.(this);
802 return;
803 }
804 this.userData = userData ?? {};
805 if (immediate) {
806 this.animatedScroll = this.targetScroll = target;
807 this.setScroll(this.scroll);
808 this.reset();
809 this.preventNextNativeScrollEvent();
810 this.emit();
811 onComplete?.(this);
812 this.userData = {};
813 requestAnimationFrame(() => {
814 this.dispatchScrollendEvent();
815 });
816 return;
817 }
818 if (!programmatic) this.targetScroll = target;
819 if (typeof duration === "number" && typeof easing !== "function") easing = defaultEasing;
820 else if (typeof easing === "function" && typeof duration !== "number") duration = 1;
821 this.animate.fromTo(this.animatedScroll, target, {
822 duration,
823 easing,
824 lerp,
825 onStart: () => {
826 if (lock) this.isLocked = true;
827 this.isScrolling = "smooth";
828 onStart?.(this);
829 },
830 onUpdate: (value, completed) => {
831 this.isScrolling = "smooth";
832 this.lastVelocity = this.velocity;
833 this.velocity = value - this.animatedScroll;
834 this.direction = Math.sign(this.velocity);
835 this.animatedScroll = value;
836 this.setScroll(this.scroll);
837 if (programmatic) this.targetScroll = value;
838 if (!completed) this.emit();
839 if (completed) {
840 this.reset();
841 this.emit();
842 onComplete?.(this);
843 this.userData = {};
844 requestAnimationFrame(() => {
845 this.dispatchScrollendEvent();
846 });
847 this.preventNextNativeScrollEvent();
848 }
849 }
850 });
851 }
852 preventNextNativeScrollEvent() {
853 this._preventNextNativeScrollEvent = true;
854 requestAnimationFrame(() => {
855 this._preventNextNativeScrollEvent = false;
856 });
857 }
858 hasNestedScroll(node, { deltaX, deltaY }) {
859 const time = Date.now();
860 if (!node._lenis) node._lenis = {};
861 const cache = node._lenis;
862 let hasOverflowX;
863 let hasOverflowY;
864 let isScrollableX;
865 let isScrollableY;
866 let hasOverscrollBehaviorX;
867 let hasOverscrollBehaviorY;
868 let scrollWidth;
869 let scrollHeight;
870 let clientWidth;
871 let clientHeight;
872 if (time - (cache.time ?? 0) > 2e3) {
873 cache.time = Date.now();
874 const computedStyle = window.getComputedStyle(node);
875 cache.computedStyle = computedStyle;
876 hasOverflowX = [
877 "auto",
878 "overlay",
879 "scroll"
880 ].includes(computedStyle.overflowX);
881 hasOverflowY = [
882 "auto",
883 "overlay",
884 "scroll"
885 ].includes(computedStyle.overflowY);
886 hasOverscrollBehaviorX = ["auto"].includes(computedStyle.overscrollBehaviorX);
887 hasOverscrollBehaviorY = ["auto"].includes(computedStyle.overscrollBehaviorY);
888 cache.hasOverflowX = hasOverflowX;
889 cache.hasOverflowY = hasOverflowY;
890 if (!(hasOverflowX || hasOverflowY)) return false;
891 scrollWidth = node.scrollWidth;
892 scrollHeight = node.scrollHeight;
893 clientWidth = node.clientWidth;
894 clientHeight = node.clientHeight;
895 isScrollableX = scrollWidth > clientWidth;
896 isScrollableY = scrollHeight > clientHeight;
897 cache.isScrollableX = isScrollableX;
898 cache.isScrollableY = isScrollableY;
899 cache.scrollWidth = scrollWidth;
900 cache.scrollHeight = scrollHeight;
901 cache.clientWidth = clientWidth;
902 cache.clientHeight = clientHeight;
903 cache.hasOverscrollBehaviorX = hasOverscrollBehaviorX;
904 cache.hasOverscrollBehaviorY = hasOverscrollBehaviorY;
905 } else {
906 isScrollableX = cache.isScrollableX;
907 isScrollableY = cache.isScrollableY;
908 hasOverflowX = cache.hasOverflowX;
909 hasOverflowY = cache.hasOverflowY;
910 scrollWidth = cache.scrollWidth;
911 scrollHeight = cache.scrollHeight;
912 clientWidth = cache.clientWidth;
913 clientHeight = cache.clientHeight;
914 hasOverscrollBehaviorX = cache.hasOverscrollBehaviorX;
915 hasOverscrollBehaviorY = cache.hasOverscrollBehaviorY;
916 }
917 if (!(hasOverflowX && isScrollableX || hasOverflowY && isScrollableY)) return false;
918 const orientation = Math.abs(deltaX) >= Math.abs(deltaY) ? "horizontal" : "vertical";
919 let scroll;
920 let maxScroll;
921 let delta;
922 let hasOverflow;
923 let isScrollable;
924 let hasOverscrollBehavior;
925 if (orientation === "horizontal") {
926 scroll = Math.round(node.scrollLeft);
927 maxScroll = scrollWidth - clientWidth;
928 delta = deltaX;
929 hasOverflow = hasOverflowX;
930 isScrollable = isScrollableX;
931 hasOverscrollBehavior = hasOverscrollBehaviorX;
932 } else if (orientation === "vertical") {
933 scroll = Math.round(node.scrollTop);
934 maxScroll = scrollHeight - clientHeight;
935 delta = deltaY;
936 hasOverflow = hasOverflowY;
937 isScrollable = isScrollableY;
938 hasOverscrollBehavior = hasOverscrollBehaviorY;
939 } else return false;
940 if (!hasOverscrollBehavior && (scroll >= maxScroll || scroll <= 0)) return true;
941 return (delta > 0 ? scroll < maxScroll : scroll > 0) && hasOverflow && isScrollable;
942 }
943 /**
944 * The root element on which lenis is instanced
945 */
946 get rootElement() {
947 return this.options.wrapper === window ? document.documentElement : this.options.wrapper;
948 }
949 /**
950 * The limit which is the maximum scroll value
951 */
952 get limit() {
953 if (this.options.naiveDimensions) {
954 if (this.isHorizontal) return this.rootElement.scrollWidth - this.rootElement.clientWidth;
955 return this.rootElement.scrollHeight - this.rootElement.clientHeight;
956 }
957 return this.dimensions.limit[this.isHorizontal ? "x" : "y"];
958 }
959 /**
960 * Whether or not the scroll is horizontal
961 */
962 get isHorizontal() {
963 return this.options.orientation === "horizontal";
964 }
965 /**
966 * The actual scroll value
967 */
968 get actualScroll() {
969 const wrapper = this.options.wrapper;
970 return this.isHorizontal ? wrapper.scrollX ?? wrapper.scrollLeft : wrapper.scrollY ?? wrapper.scrollTop;
971 }
972 /**
973 * The current scroll value
974 */
975 get scroll() {
976 return this.options.infinite ? modulo(this.animatedScroll, this.limit) : this.animatedScroll;
977 }
978 /**
979 * The progress of the scroll relative to the limit
980 */
981 get progress() {
982 return this.limit === 0 ? 1 : this.scroll / this.limit;
983 }
984 /**
985 * Current scroll state
986 */
987 get isScrolling() {
988 return this._isScrolling;
989 }
990 set isScrolling(value) {
991 if (this._isScrolling !== value) {
992 this._isScrolling = value;
993 this.updateClassName();
994 }
995 }
996 /**
997 * Check if lenis is stopped
998 */
999 get isStopped() {
1000 return this._isStopped;
1001 }
1002 set isStopped(value) {
1003 if (this._isStopped !== value) {
1004 this._isStopped = value;
1005 this.updateClassName();
1006 }
1007 }
1008 /**
1009 * Check if lenis is locked
1010 */
1011 get isLocked() {
1012 return this._isLocked;
1013 }
1014 set isLocked(value) {
1015 if (this._isLocked !== value) {
1016 this._isLocked = value;
1017 this.updateClassName();
1018 }
1019 }
1020 /**
1021 * Check if lenis is smooth scrolling
1022 */
1023 get isSmooth() {
1024 return this.isScrolling === "smooth";
1025 }
1026 /**
1027 * Whether the user prefers reduced motion and lenis is honoring it (see `respectReducedMotion` option)
1028 */
1029 get prefersReducedMotion() {
1030 return this.options.respectReducedMotion && this.reducedMotionMediaQuery.matches;
1031 }
1032 /**
1033 * The class name applied to the wrapper element
1034 */
1035 get className() {
1036 let className = "lenis";
1037 if (this.options.autoToggle) className += " lenis-autoToggle";
1038 if (this.isStopped) className += " lenis-stopped";
1039 if (this.isLocked) className += " lenis-locked";
1040 if (this.isScrolling) className += " lenis-scrolling";
1041 if (this.isScrolling === "smooth") className += " lenis-smooth";
1042 return className;
1043 }
1044 updateClassName() {
1045 this.cleanUpClassName();
1046 this.className.split(" ").forEach((className) => {
1047 this.rootElement.classList.add(className);
1048 });
1049 }
1050 cleanUpClassName() {
1051 for (const className of Array.from(this.rootElement.classList)) if (className === "lenis" || className.startsWith("lenis-")) this.rootElement.classList.remove(className);
1052 }
1053};
1054//#endregion
1055export { Lenis as default };
1056
1057//# sourceMappingURL=lenis.mjs.map
Note: See TracBrowser for help on using the repository browser.