Files
OverlayScrollbars/packages/overlayscrollbars/src/environment.ts
T
2022-07-24 21:50:34 +02:00

218 lines
7.3 KiB
TypeScript

import {
createDOM,
addClass,
style,
appendChildren,
fractionalSize,
clientSize,
absoluteCoordinates,
offsetSize,
scrollLeft,
XY,
removeAttr,
removeElements,
equalBCRWH,
getBoundingClientRect,
assignDeep,
cssProperty,
createCache,
equalXY,
createEventListenerHub,
EventListener,
} from 'support';
import {
classNameEnvironment,
classNameEnvironmentFlexboxGlue,
classNameEnvironmentFlexboxGlueMax,
classNameViewportScrollbarHidden,
} from 'classnames';
import { Options, defaultOptions } from 'options';
import { DeepPartial } from 'typings';
import { Initialization } from 'initialization';
import { getPlugins, ScrollbarsHidingPluginInstance, scrollbarsHidingPluginName } from 'plugins';
type EnvironmentEventMap = {
_: [];
};
export interface InternalEnvironment {
readonly _nativeScrollbarsSize: XY;
readonly _nativeScrollbarsOverlaid: XY<boolean>;
readonly _nativeScrollbarsHiding: boolean;
readonly _rtlScrollBehavior: { n: boolean; i: boolean };
readonly _flexboxGlue: boolean;
readonly _cssCustomProperties: boolean;
readonly _staticDefaultInitialization: Initialization;
readonly _staticDefaultOptions: Options;
_addListener(listener: EventListener<EnvironmentEventMap, '_'>): () => void;
_getDefaultInitialization(): Initialization;
_setDefaultInitialization(newInitialization: DeepPartial<Initialization>): void;
_getDefaultOptions(): Options;
_setDefaultOptions(newDefaultOptions: DeepPartial<Options>): void;
}
let environmentInstance: InternalEnvironment;
const getNativeScrollbarSize = (
body: HTMLElement,
measureElm: HTMLElement,
measureElmChild: HTMLElement,
clear?: boolean
): XY => {
appendChildren(body, measureElm);
const cSize = clientSize(measureElm);
const oSize = offsetSize(measureElm);
const fSize = fractionalSize(measureElmChild);
clear && removeElements(measureElm);
return {
x: oSize.h - cSize.h + fSize.h,
y: oSize.w - cSize.w + fSize.w,
};
};
const getNativeScrollbarsHiding = (testElm: HTMLElement): boolean => {
let result = false;
const revertClass = addClass(testElm, classNameViewportScrollbarHidden);
try {
result =
style(testElm, cssProperty('scrollbar-width')) === 'none' ||
window.getComputedStyle(testElm, '::-webkit-scrollbar').getPropertyValue('display') ===
'none';
} catch (ex) {}
revertClass();
return result;
};
const getRtlScrollBehavior = (
parentElm: HTMLElement,
childElm: HTMLElement
): { i: boolean; n: boolean } => {
const strHidden = 'hidden';
style(parentElm, { overflowX: strHidden, overflowY: strHidden, direction: 'rtl' });
scrollLeft(parentElm, 0);
const parentOffset = absoluteCoordinates(parentElm);
const childOffset = absoluteCoordinates(childElm);
scrollLeft(parentElm, -999); // https://github.com/KingSora/OverlayScrollbars/issues/187
const childOffsetAfterScroll = absoluteCoordinates(childElm);
return {
/**
* origin direction = determines if the zero scroll position is on the left or right side
* 'i' means 'invert' (i === true means that the axis must be inverted to be correct)
* true = on the left side
* false = on the right side
*/
i: parentOffset.x === childOffset.x,
/**
* negative = determines if the maximum scroll is positive or negative
* 'n' means 'negate' (n === true means that the axis must be negated to be correct)
* true = negative
* false = positive
*/
n: childOffset.x !== childOffsetAfterScroll.x,
};
};
const getFlexboxGlue = (parentElm: HTMLElement, childElm: HTMLElement): boolean => {
const revertFbxGlue = addClass(parentElm, classNameEnvironmentFlexboxGlue);
const minOffsetsizeParent = getBoundingClientRect(parentElm);
const minOffsetsize = getBoundingClientRect(childElm);
const supportsMin = equalBCRWH(minOffsetsize, minOffsetsizeParent, true);
const revertFbxGlueMax = addClass(parentElm, classNameEnvironmentFlexboxGlueMax);
const maxOffsetsizeParent = getBoundingClientRect(parentElm);
const maxOffsetsize = getBoundingClientRect(childElm);
const supportsMax = equalBCRWH(maxOffsetsize, maxOffsetsizeParent, true);
revertFbxGlue();
revertFbxGlueMax();
return supportsMin && supportsMax;
};
const createEnvironment = (): InternalEnvironment => {
const { body } = document;
const envDOM = createDOM(`<div class="${classNameEnvironment}"><div></div></div>`);
const envElm = envDOM[0] as HTMLElement;
const envChildElm = envElm.firstChild as HTMLElement;
const [addEvent, , triggerEvent] = createEventListenerHub<EnvironmentEventMap>();
const [updateNativeScrollbarSizeCache, getNativeScrollbarSizeCache] = createCache(
{
_initialValue: getNativeScrollbarSize(body, envElm, envChildElm),
_equal: equalXY,
},
getNativeScrollbarSize.bind(0, body, envElm, envChildElm, true)
);
const [nativeScrollbarsSize] = getNativeScrollbarSizeCache();
const nativeScrollbarsHiding = getNativeScrollbarsHiding(envElm);
const nativeScrollbarsOverlaid = {
x: nativeScrollbarsSize.x === 0,
y: nativeScrollbarsSize.y === 0,
};
const staticDefaultInitialization: Initialization = {
host: null,
padding: !nativeScrollbarsHiding,
viewport: (target) => nativeScrollbarsHiding && target === target.ownerDocument.body && target,
content: false,
scrollbarsSlot: true,
cancel: {
nativeScrollbarsOverlaid: true,
body: null,
},
};
const staticDefaultOptions = assignDeep({}, defaultOptions);
const env: InternalEnvironment = {
_nativeScrollbarsSize: nativeScrollbarsSize,
_nativeScrollbarsOverlaid: nativeScrollbarsOverlaid,
_nativeScrollbarsHiding: nativeScrollbarsHiding,
_cssCustomProperties: style(envElm, 'zIndex') === '-1',
_rtlScrollBehavior: getRtlScrollBehavior(envElm, envChildElm),
_flexboxGlue: getFlexboxGlue(envElm, envChildElm),
_addListener: (listener) => addEvent('_', listener),
_getDefaultInitialization: assignDeep<Initialization, Initialization>.bind(
0,
{} as Initialization,
staticDefaultInitialization
),
_setDefaultInitialization(newInitializationStrategy) {
assignDeep(staticDefaultInitialization, newInitializationStrategy);
},
_getDefaultOptions: assignDeep<Options, Options>.bind(0, {} as Options, staticDefaultOptions),
_setDefaultOptions(newDefaultOptions) {
assignDeep(staticDefaultOptions, newDefaultOptions);
},
_staticDefaultInitialization: assignDeep({}, staticDefaultInitialization),
_staticDefaultOptions: assignDeep({}, staticDefaultOptions),
};
removeAttr(envElm, 'style');
removeElements(envElm);
if (!nativeScrollbarsHiding && (!nativeScrollbarsOverlaid.x || !nativeScrollbarsOverlaid.y)) {
let resizeFn: undefined | ReturnType<ScrollbarsHidingPluginInstance['_envWindowZoom']>;
window.addEventListener('resize', () => {
const scrollbarsHidingPlugin = getPlugins()[scrollbarsHidingPluginName] as
| ScrollbarsHidingPluginInstance
| undefined;
resizeFn = resizeFn || (scrollbarsHidingPlugin && scrollbarsHidingPlugin._envWindowZoom());
resizeFn && resizeFn(env, updateNativeScrollbarSizeCache, triggerEvent.bind(0, '_'));
});
}
return env;
};
const getEnvironment = (): InternalEnvironment => {
if (!environmentInstance) {
environmentInstance = createEnvironment();
}
return environmentInstance;
};
export { getEnvironment };