import type { DomTools } from './domtools.classes.domtools.js'; import * as plugins from './domtools.plugins.js'; export class Scroller { public domtoolsInstance: DomTools; // Array to store scroll callback functions. private scrollCallbacks: Array<() => void> = []; // Lenis instance (if activated) or null. private lenisInstance: plugins.Lenis | null = null; // Bound handlers to allow removal from event listeners. private handleNativeScroll = (event: Event): void => { this.executeScrollCallbacks(); }; private handleLenisScroll = (info: any): void => { this.executeScrollCallbacks(); }; constructor(domtoolsInstanceArg: DomTools) { this.domtoolsInstance = domtoolsInstanceArg; // Attach the native scroll listener by default. this.attachNativeScrollListener(); } private sweetScroller = new plugins.SweetScroll({}); /** * Scrolls to a given element with options. */ public async scrollToElement( elementArg: HTMLElement, optionsArg: Parameters[1] ) { this.sweetScroller.toElement(elementArg, optionsArg); await plugins.smartdelay.delayFor(optionsArg.duration); } /** * Detects whether native smooth scrolling is enabled. */ public async detectNativeSmoothScroll() { const done = plugins.smartpromise.defer(); const sampleSize = 100; const acceptableDeltaDifference = 3; const minimumSmoothRatio = 0.75; const eventDeltas: number[] = []; function onWheel(event: WheelEvent) { eventDeltas.push(event.deltaY); if (eventDeltas.length >= sampleSize) { window.removeEventListener('wheel', onWheel); analyzeEvents(); } } function analyzeEvents() { const totalDiffs = eventDeltas.length - 1; let smallDiffCount = 0; for (let i = 0; i < totalDiffs; i++) { const diff = Math.abs(eventDeltas[i + 1] - eventDeltas[i]); if (diff <= acceptableDeltaDifference) { smallDiffCount++; } } const smoothRatio = smallDiffCount / totalDiffs; if (smoothRatio >= minimumSmoothRatio) { console.log('Smooth scrolling detected.'); done.resolve(true); } else { console.log('Smooth scrolling NOT detected.'); done.resolve(false); } } window.addEventListener('wheel', onWheel); return done.promise; } /** * Enables Lenis scrolling. * If optionsArg.disableOnNativeSmoothScroll is true and native smooth scrolling is detected, * Lenis will be destroyed immediately. */ public async enableLenisScroll(optionsArg?: { disableOnNativeSmoothScroll?: boolean }) { const lenis = new plugins.Lenis({ autoRaf: true, }); if (optionsArg?.disableOnNativeSmoothScroll) { if (await this.detectNativeSmoothScroll()) { lenis.destroy(); return; } } // Activate Lenis scrolling. this.lenisInstance = lenis; // Switch from native scroll listener to Lenis scroll listener. this.detachNativeScrollListener(); this.attachLenisScrollListener(); // Monkey-patch the destroy method so that when Lenis is destroyed, // the native scroll listener is reattached. const originalDestroy = lenis.destroy.bind(lenis); lenis.destroy = () => { originalDestroy(); this.detachLenisScrollListener(); this.attachNativeScrollListener(); this.lenisInstance = null; }; } /** * Registers a callback to be executed on scroll. * @param callback A function to execute on each scroll event. */ public onScroll(callback: () => void): void { this.scrollCallbacks.push(callback); } /** * Executes all registered scroll callbacks concurrently. */ private executeScrollCallbacks(): void { // Execute all callbacks in parallel. this.scrollCallbacks.forEach((callback) => { try { callback(); } catch (error) { console.error('Error in scroll callback:', error); } }); } /** * Attaches the native scroll event listener. */ private attachNativeScrollListener(): void { window.addEventListener('scroll', this.handleNativeScroll); } /** * Detaches the native scroll event listener. */ private detachNativeScrollListener(): void { window.removeEventListener('scroll', this.handleNativeScroll); } /** * Attaches the Lenis scroll event listener. */ private attachLenisScrollListener(): void { if (this.lenisInstance) { // Assuming that Lenis exposes an `on` method to listen to scroll events. this.lenisInstance.on('scroll', this.handleLenisScroll); } } /** * Detaches the Lenis scroll event listener. */ private detachLenisScrollListener(): void { if (this.lenisInstance) { // Assuming that Lenis exposes an `off` method to remove scroll event listeners. this.lenisInstance.off('scroll', this.handleLenisScroll); } } }