mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-08 03:42:27 +03:00
162 lines
5.7 KiB
TypeScript
162 lines
5.7 KiB
TypeScript
import {
|
|
createDOM,
|
|
style,
|
|
appendChildren,
|
|
offsetSize,
|
|
scrollLeft,
|
|
scrollTop,
|
|
jsAPI,
|
|
runEach,
|
|
prependChildren,
|
|
removeElements,
|
|
on,
|
|
preventDefault,
|
|
stopPropagation,
|
|
addClass,
|
|
isString,
|
|
equalWH,
|
|
} from 'support';
|
|
import { getEnvironment } from 'environment';
|
|
|
|
const animationStartEventName = 'animationstart';
|
|
const scrollEventName = 'scroll';
|
|
const scrollAmount = 3333333;
|
|
const ResizeObserverConstructor = jsAPI('ResizeObserver');
|
|
const classNameSizeObserver = 'os-size-observer';
|
|
const classNameSizeObserverAppear = `${classNameSizeObserver}-appear`;
|
|
const classNameSizeObserverListener = `${classNameSizeObserver}-listener`;
|
|
const classNameSizeObserverListenerScroll = `${classNameSizeObserverListener}-scroll`;
|
|
const classNameSizeObserverListenerItem = `${classNameSizeObserverListener}-item`;
|
|
const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`;
|
|
const cAF = cancelAnimationFrame;
|
|
const rAF = requestAnimationFrame;
|
|
const getDirection = (elm: HTMLElement) => style(elm, 'direction');
|
|
|
|
// TODO:
|
|
// 1. MAYBE add comparison function to offsetSize etc.
|
|
type Direction = 'ltr' | 'rtl';
|
|
export type SizeObserverOptions = { _direction?: boolean; _appear?: boolean };
|
|
export const createSizeObserver = (
|
|
target: HTMLElement,
|
|
onSizeChangedCallback: (direction?: Direction) => any,
|
|
options?: SizeObserverOptions
|
|
): (() => void) => {
|
|
const { _direction: direction = false, _appear: appear = false } = options || {};
|
|
const rtlScrollBehavior = getEnvironment()._rtlScrollBehavior;
|
|
const baseElements = createDOM(`<div class="${classNameSizeObserver}"><div class="${classNameSizeObserverListener}"></div></div>`);
|
|
const sizeObserver = baseElements[0] as HTMLElement;
|
|
const listenerElement = sizeObserver.firstChild as HTMLElement;
|
|
const onSizeChangedCallbackProxy = (dir?: Direction) => {
|
|
if (direction) {
|
|
const rtl = getDirection(sizeObserver) === 'rtl';
|
|
scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount);
|
|
scrollTop(sizeObserver, scrollAmount);
|
|
}
|
|
onSizeChangedCallback(isString(dir) ? dir : undefined);
|
|
};
|
|
const offListeners: (() => void)[] = [];
|
|
let appearCallback: ((...args: any) => any) | null = appear ? onSizeChangedCallbackProxy : null;
|
|
|
|
if (ResizeObserverConstructor) {
|
|
const resizeObserverInstance = new ResizeObserverConstructor(onSizeChangedCallbackProxy);
|
|
resizeObserverInstance.observe(listenerElement);
|
|
offListeners.push(() => resizeObserverInstance.disconnect());
|
|
} else {
|
|
const observerElementChildren = createDOM(
|
|
`<div class="${classNameSizeObserverListenerItem}" dir="ltr"><div class="${classNameSizeObserverListenerItem}"><div class="${classNameSizeObserverListenerItemFinal}"></div></div><div class="${classNameSizeObserverListenerItem}"><div class="${classNameSizeObserverListenerItemFinal}" style="width: 200%; height: 200%"></div></div></div>`
|
|
);
|
|
appendChildren(listenerElement, observerElementChildren);
|
|
addClass(listenerElement, classNameSizeObserverListenerScroll);
|
|
const observerElementChildrenRoot = observerElementChildren[0] as HTMLElement;
|
|
const shrinkElement = observerElementChildrenRoot.lastChild as HTMLElement;
|
|
const expandElement = observerElementChildrenRoot.firstChild as HTMLElement;
|
|
const expandElementChild = expandElement?.firstChild as HTMLElement;
|
|
|
|
let cacheSize = offsetSize(listenerElement);
|
|
let currSize = cacheSize;
|
|
let isDirty = false;
|
|
let rAFId: number;
|
|
|
|
const reset = () => {
|
|
scrollLeft(expandElement, scrollAmount);
|
|
scrollTop(expandElement, scrollAmount);
|
|
scrollLeft(shrinkElement, scrollAmount);
|
|
scrollTop(shrinkElement, scrollAmount);
|
|
};
|
|
const onResized = function () {
|
|
rAFId = 0;
|
|
if (!isDirty) {
|
|
return;
|
|
}
|
|
|
|
cacheSize = currSize;
|
|
onSizeChangedCallbackProxy();
|
|
};
|
|
const onScroll = (scrollEvent?: Event) => {
|
|
currSize = offsetSize(listenerElement);
|
|
isDirty = !scrollEvent || !equalWH(currSize, cacheSize);
|
|
|
|
if (scrollEvent && isDirty && !rAFId) {
|
|
cAF(rAFId);
|
|
rAFId = rAF(onResized);
|
|
} else if (!scrollEvent) {
|
|
onResized();
|
|
}
|
|
|
|
reset();
|
|
if (scrollEvent) {
|
|
preventDefault(scrollEvent);
|
|
stopPropagation(scrollEvent);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
offListeners.push(on(expandElement, scrollEventName, onScroll));
|
|
offListeners.push(on(shrinkElement, scrollEventName, onScroll));
|
|
|
|
// lets assume that the divs will never be that large and a constant value is enough
|
|
style(expandElementChild, {
|
|
width: scrollAmount,
|
|
height: scrollAmount,
|
|
});
|
|
reset();
|
|
appearCallback = appear ? () => onScroll() : reset;
|
|
}
|
|
|
|
if (direction) {
|
|
let dirCache: string | undefined;
|
|
offListeners.push(
|
|
on(sizeObserver, scrollEventName, (event: Event) => {
|
|
const dir = getDirection(sizeObserver);
|
|
const changed = dir !== dirCache;
|
|
if (changed) {
|
|
if (dir === 'rtl') {
|
|
style(listenerElement, { left: 'auto', right: 0 });
|
|
} else {
|
|
style(listenerElement, { left: 0, right: 'auto' });
|
|
}
|
|
dirCache = dir;
|
|
onSizeChangedCallbackProxy(dir as Direction);
|
|
}
|
|
|
|
preventDefault(event);
|
|
stopPropagation(event);
|
|
return false;
|
|
})
|
|
);
|
|
}
|
|
|
|
// appearCallback is always needed on scroll-observer strategy to reset it
|
|
if (appearCallback) {
|
|
addClass(sizeObserver, classNameSizeObserverAppear);
|
|
offListeners.push(on(sizeObserver, animationStartEventName, appearCallback));
|
|
}
|
|
|
|
prependChildren(target, sizeObserver);
|
|
|
|
return () => {
|
|
runEach(offListeners);
|
|
removeElements(sizeObserver);
|
|
};
|
|
};
|