229 lines
6.1 KiB
TypeScript
229 lines
6.1 KiB
TypeScript
import type { DomTools } from './domtools.classes.domtools.js';
|
|
import * as plugins from './domtools.plugins.js';
|
|
|
|
export class Scroller {
|
|
public domtoolsInstance: DomTools;
|
|
private disposed = false;
|
|
|
|
// Array to store scroll callback functions.
|
|
private scrollCallbacks: Array<() => void> = [];
|
|
|
|
// Lenis instance (if activated) or null.
|
|
private lenisInstance: plugins.Lenis | null = null;
|
|
private lenisScrollUnsubscribe: (() => void) | null = null;
|
|
private nativeScrollListenerAttached = false;
|
|
|
|
// Bound handlers to allow removal from event listeners.
|
|
private handleNativeScroll = (_event: Event): void => {
|
|
this.executeScrollCallbacks();
|
|
};
|
|
|
|
private handleLenisScroll = (_info: plugins.Lenis): 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 toElement(
|
|
elementArg: HTMLElement,
|
|
optionsArg: Parameters<typeof this.sweetScroller.toElement>[1]
|
|
) {
|
|
this.sweetScroller.toElement(elementArg, optionsArg);
|
|
if (optionsArg?.duration) {
|
|
await plugins.smartdelay.delayFor(optionsArg.duration);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detects whether native smooth scrolling is enabled.
|
|
*/
|
|
public async detectNativeSmoothScroll() {
|
|
const rootScrollBehavior = getComputedStyle(document.documentElement).scrollBehavior;
|
|
const bodyScrollBehavior = document.body ? getComputedStyle(document.body).scrollBehavior : 'auto';
|
|
if (rootScrollBehavior === 'smooth' || bodyScrollBehavior === 'smooth') {
|
|
return true;
|
|
}
|
|
|
|
const done = plugins.smartpromise.defer<boolean>();
|
|
const sampleSize = 12;
|
|
const acceptableDeltaDifference = 3;
|
|
const minimumSmoothRatio = 0.75;
|
|
const timeoutInMs = 1200;
|
|
|
|
const eventDeltas: number[] = [];
|
|
|
|
const finalize = (result: boolean) => {
|
|
window.removeEventListener('wheel', onWheel);
|
|
window.clearTimeout(timeoutId);
|
|
done.resolve(result);
|
|
};
|
|
|
|
function onWheel(event: WheelEvent) {
|
|
eventDeltas.push(event.deltaY);
|
|
|
|
if (eventDeltas.length >= sampleSize) {
|
|
analyzeEvents();
|
|
}
|
|
}
|
|
|
|
function analyzeEvents() {
|
|
if (eventDeltas.length < 2) {
|
|
finalize(false);
|
|
return;
|
|
}
|
|
|
|
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;
|
|
finalize(smoothRatio >= minimumSmoothRatio);
|
|
}
|
|
|
|
const timeoutId = window.setTimeout(() => {
|
|
analyzeEvents();
|
|
}, timeoutInMs);
|
|
|
|
window.addEventListener('wheel', onWheel, { passive: true });
|
|
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 }) {
|
|
if (this.disposed) {
|
|
return;
|
|
}
|
|
|
|
if (this.lenisInstance) {
|
|
this.disableLenisScroll();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
public disableLenisScroll() {
|
|
if (!this.lenisInstance) {
|
|
return;
|
|
}
|
|
|
|
this.detachLenisScrollListener();
|
|
this.lenisInstance.destroy();
|
|
this.lenisInstance = null;
|
|
this.attachNativeScrollListener();
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
return () => {
|
|
this.scrollCallbacks = this.scrollCallbacks.filter((existingCallback) => existingCallback !== 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 {
|
|
if (this.nativeScrollListenerAttached || this.disposed) {
|
|
return;
|
|
}
|
|
window.addEventListener('scroll', this.handleNativeScroll);
|
|
this.nativeScrollListenerAttached = true;
|
|
}
|
|
|
|
/**
|
|
* Detaches the native scroll event listener.
|
|
*/
|
|
private detachNativeScrollListener(): void {
|
|
if (!this.nativeScrollListenerAttached) {
|
|
return;
|
|
}
|
|
window.removeEventListener('scroll', this.handleNativeScroll);
|
|
this.nativeScrollListenerAttached = false;
|
|
}
|
|
|
|
/**
|
|
* Attaches the Lenis scroll event listener.
|
|
*/
|
|
private attachLenisScrollListener(): void {
|
|
if (this.lenisInstance && !this.lenisScrollUnsubscribe) {
|
|
this.lenisScrollUnsubscribe = this.lenisInstance.on('scroll', this.handleLenisScroll);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detaches the Lenis scroll event listener.
|
|
*/
|
|
private detachLenisScrollListener(): void {
|
|
if (this.lenisScrollUnsubscribe) {
|
|
this.lenisScrollUnsubscribe();
|
|
this.lenisScrollUnsubscribe = null;
|
|
}
|
|
}
|
|
|
|
public dispose() {
|
|
if (this.disposed) {
|
|
return;
|
|
}
|
|
|
|
this.disposed = true;
|
|
this.detachLenisScrollListener();
|
|
this.lenisInstance?.destroy();
|
|
this.lenisInstance = null;
|
|
this.detachNativeScrollListener();
|
|
this.scrollCallbacks = [];
|
|
this.sweetScroller.destroy();
|
|
}
|
|
}
|