2023-10-23 14:57:58 +00:00
|
|
|
type GenericCallback = (this: unknown, ...args: unknown[]) => unknown;
|
|
|
|
|
|
|
|
type MethodOf<T, K extends keyof T> = T[K] extends GenericCallback ? T[K] : never;
|
|
|
|
|
|
|
|
const debounce = <T extends GenericCallback>(fn: T, delay: number) => {
|
|
|
|
let timerId = 0;
|
|
|
|
|
|
|
|
return function (this: unknown, ...args: unknown[]) {
|
|
|
|
window.clearTimeout(timerId);
|
|
|
|
timerId = window.setTimeout(() => {
|
|
|
|
fn.call(this, ...args);
|
|
|
|
}, delay);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const decorate = <T, M extends keyof T>(
|
|
|
|
proto: T,
|
|
|
|
method: M,
|
|
|
|
decorateFn: (this: unknown, superFn: T[M], ...args: unknown[]) => unknown
|
|
|
|
) => {
|
|
|
|
const superFn = proto[method] as MethodOf<T, M>;
|
|
|
|
|
|
|
|
proto[method] = function (this: unknown, ...args: unknown[]) {
|
|
|
|
superFn.call(this, ...args);
|
|
|
|
decorateFn.call(this, superFn, ...args);
|
|
|
|
} as MethodOf<T, M>;
|
|
|
|
};
|
|
|
|
|
|
|
|
const isSupported = 'onscrollend' in window;
|
|
|
|
|
|
|
|
if (!isSupported) {
|
|
|
|
const pointers = new Set();
|
|
|
|
const scrollHandlers = new WeakMap<EventTarget, EventListenerOrEventListenerObject>();
|
|
|
|
|
2024-01-23 15:39:48 +00:00
|
|
|
const handlePointerDown = (event: TouchEvent) => {
|
|
|
|
for (const touch of event.changedTouches) {
|
|
|
|
pointers.add(touch.identifier);
|
|
|
|
}
|
2023-10-23 14:57:58 +00:00
|
|
|
};
|
|
|
|
|
2024-01-23 15:39:48 +00:00
|
|
|
const handlePointerUp = (event: TouchEvent) => {
|
|
|
|
for (const touch of event.changedTouches) {
|
|
|
|
pointers.delete(touch.identifier);
|
|
|
|
}
|
2023-10-23 14:57:58 +00:00
|
|
|
};
|
|
|
|
|
2024-01-23 15:39:48 +00:00
|
|
|
document.addEventListener('touchstart', handlePointerDown, true);
|
|
|
|
document.addEventListener('touchend', handlePointerUp, true);
|
|
|
|
document.addEventListener('touchcancel', handlePointerUp, true);
|
2023-10-23 14:57:58 +00:00
|
|
|
|
|
|
|
decorate(EventTarget.prototype, 'addEventListener', function (this: EventTarget, addEventListener, type) {
|
2024-01-23 15:39:48 +00:00
|
|
|
if (type !== 'scrollend') return;
|
2023-10-23 14:57:58 +00:00
|
|
|
|
|
|
|
const handleScrollEnd = debounce(() => {
|
|
|
|
if (!pointers.size) {
|
|
|
|
// If no pointer is active in the scroll area then the scroll has ended
|
|
|
|
this.dispatchEvent(new Event('scrollend'));
|
|
|
|
} else {
|
|
|
|
// otherwise let's wait a bit more
|
|
|
|
handleScrollEnd();
|
|
|
|
}
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
addEventListener.call(this, 'scroll', handleScrollEnd, { passive: true });
|
|
|
|
scrollHandlers.set(this, handleScrollEnd);
|
|
|
|
});
|
|
|
|
|
|
|
|
decorate(EventTarget.prototype, 'removeEventListener', function (this: EventTarget, removeEventListener, type) {
|
2024-01-23 15:39:48 +00:00
|
|
|
if (type !== 'scrollend') return;
|
2023-10-23 14:57:58 +00:00
|
|
|
|
|
|
|
const scrollHandler = scrollHandlers.get(this);
|
|
|
|
if (scrollHandler) {
|
|
|
|
removeEventListener.call(this, 'scroll', scrollHandler, { passive: true } as unknown as EventListenerOptions);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2023-10-23 15:55:28 +00:00
|
|
|
|
|
|
|
// Without an import or export, TypeScript sees vars in this file as global
|
|
|
|
export {};
|