improve sizeobserver with passive listeners, fix raf and setT code, move part of enviroment code to plugin

This commit is contained in:
Rene
2022-07-17 21:32:24 +02:00
parent 2704b66f69
commit d567cae275
15 changed files with 137 additions and 124 deletions
+18 -62
View File
@@ -11,7 +11,6 @@ import {
XY,
removeAttr,
removeElements,
windowSize,
equalBCRWH,
getBoundingClientRect,
assignDeep,
@@ -30,6 +29,7 @@ import {
import { Options, defaultOptions } from 'options';
import { PartialOptions } from 'typings';
import { InitializationStrategy } from 'initialization';
import { getPlugins, ScrollbarsHidingPluginInstance, scrollbarsHidingPluginName } from 'plugins';
type EnvironmentEventMap = {
_: [];
@@ -52,18 +52,12 @@ export interface InternalEnvironment {
}
let environmentInstance: InternalEnvironment;
const { abs, round } = Math;
const diffBiggerThanOne = (valOne: number, valTwo: number): boolean => {
const absValOne = abs(valOne);
const absValTwo = abs(valTwo);
return !(absValOne === absValTwo || absValOne + 1 === absValTwo || absValOne - 1 === absValTwo);
};
const getNativeScrollbarSize = (
body: HTMLElement,
measureElm: HTMLElement,
measureElmChild: HTMLElement
measureElmChild: HTMLElement,
clear?: boolean
): XY => {
appendChildren(body, measureElm);
@@ -71,6 +65,8 @@ const getNativeScrollbarSize = (
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,
@@ -137,26 +133,19 @@ const getFlexboxGlue = (parentElm: HTMLElement, childElm: HTMLElement): boolean
return supportsMin && supportsMax;
};
const getWindowDPR = (): number => {
// eslint-disable-next-line
// @ts-ignore
const dDPI = window.screen.deviceXDPI || 0;
// eslint-disable-next-line
// @ts-ignore
const sDPI = window.screen.logicalXDPI || 1;
return window.devicePixelRatio || dDPI / sDPI;
};
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,
});
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 = {
@@ -197,47 +186,14 @@ const createEnvironment = (): InternalEnvironment => {
removeElements(envElm);
if (!nativeScrollbarsHiding && (!nativeScrollbarsOverlaid.x || !nativeScrollbarsOverlaid.y)) {
let size = windowSize();
let dpr = getWindowDPR();
let resizeFn: undefined | ReturnType<ScrollbarsHidingPluginInstance['_envWindowZoom']>;
window.addEventListener('resize', () => {
const sizeNew = windowSize();
const deltaSize = {
w: sizeNew.w - size.w,
h: sizeNew.h - size.h,
};
const scrollbarsHidingPlugin = getPlugins()[scrollbarsHidingPluginName] as
| ScrollbarsHidingPluginInstance
| undefined;
if (deltaSize.w === 0 && deltaSize.h === 0) return;
const deltaAbsSize = {
w: abs(deltaSize.w),
h: abs(deltaSize.h),
};
const deltaAbsRatio = {
w: abs(round(sizeNew.w / (size.w / 100.0))),
h: abs(round(sizeNew.h / (size.h / 100.0))),
};
const dprNew = getWindowDPR();
const deltaIsBigger = deltaAbsSize.w > 2 && deltaAbsSize.h > 2;
const difference = !diffBiggerThanOne(deltaAbsRatio.w, deltaAbsRatio.h);
const dprChanged = dprNew !== dpr && dpr > 0;
const isZoom = deltaIsBigger && difference && dprChanged;
if (isZoom) {
const [scrollbarSize, scrollbarSizeChanged] = updateNativeScrollbarSizeCache(
getNativeScrollbarSize(body, envElm, envChildElm)
);
assignDeep(environmentInstance._nativeScrollbarsSize, scrollbarSize); // keep the object same!
removeElements(envElm);
if (scrollbarSizeChanged) {
triggerEvent('_');
}
}
size = sizeNew;
dpr = dprNew;
resizeFn = resizeFn || (scrollbarsHidingPlugin && scrollbarsHidingPlugin._envWindowZoom());
resizeFn && resizeFn(env, updateNativeScrollbarSizeCache, triggerEvent.bind(0, '_'));
});
}
@@ -10,7 +10,6 @@ import {
prependChildren,
removeElements,
on,
stopAndPrevent,
addClass,
push,
ResizeObserverConstructor,
@@ -18,6 +17,7 @@ import {
isBoolean,
removeClass,
isObject,
stopPropagation,
} from 'support';
import { getEnvironment } from 'environment';
import {
@@ -190,7 +190,7 @@ export const createSizeObserver = (
onSizeChangedCallbackProxy(directionIsRTLCacheValues);
}
stopAndPrevent(event);
stopPropagation(event);
})
);
}
@@ -12,9 +12,10 @@ const pluginRegistry: Record<string, PluginInstance> = {};
export const getPlugins = () => assignDeep({}, pluginRegistry);
export const addPlugin = (addedPlugin: Plugin | Plugin[]) =>
export const addPlugin = (addedPlugin: Plugin | Plugin[]) => {
each((isArray(addedPlugin) ? addedPlugin : [addedPlugin]) as Plugin[], (plugin) => {
each(keys(plugin), (pluginName) => {
pluginRegistry[pluginName] = plugin[pluginName];
});
});
};
@@ -1,5 +1,17 @@
import { keys, attr, WH, style, addClass, removeClass, noop, each } from 'support';
import { getEnvironment } from 'environment';
import {
keys,
attr,
WH,
style,
addClass,
removeClass,
noop,
each,
assignDeep,
windowSize,
UpdateCache,
XY,
} from 'support';
import { classNameViewportArrange } from 'classnames';
import type { StyleObject } from 'typings';
import type { StructureSetupState } from 'setups/structureSetup';
@@ -8,6 +20,7 @@ import type {
GetViewportOverflowState,
HideNativeScrollbars,
} from 'setups/structureSetup/updateSegments/overflowUpdateSegment';
import type { InternalEnvironment } from 'environment';
import type { Plugin } from 'plugins';
export type ArrangeViewport = (
@@ -29,29 +42,51 @@ export type UndoArrangeViewport = (
) => UndoViewportArrangeResult;
export type ScrollbarsHidingPluginInstance = {
_createUniqueViewportArrangeElement(): HTMLStyleElement | false;
_createUniqueViewportArrangeElement(env: InternalEnvironment): HTMLStyleElement | false;
_overflowUpdateSegment(
doViewportArrange: boolean,
flexboxGlue: boolean,
viewport: HTMLElement,
viewportArrange: HTMLStyleElement | false | null | undefined,
getState: () => StructureSetupState,
getViewportOverflowState: GetViewportOverflowState,
hideNativeScrollbars: HideNativeScrollbars
): [ArrangeViewport, UndoArrangeViewport];
_envWindowZoom(): (
envInstance: InternalEnvironment,
updateNativeScrollbarSizeCache: UpdateCache<XY<number>>,
triggerEvent: () => void
) => void;
};
let contentArrangeCounter = 0;
const { round, abs } = Math;
const getWindowDPR = (): number => {
// eslint-disable-next-line
// @ts-ignore
const dDPI = window.screen.deviceXDPI || 0;
// eslint-disable-next-line
// @ts-ignore
const sDPI = window.screen.logicalXDPI || 1;
return window.devicePixelRatio || dDPI / sDPI;
};
const diffBiggerThanOne = (valOne: number, valTwo: number): boolean => {
const absValOne = abs(valOne);
const absValTwo = abs(valTwo);
return !(absValOne === absValTwo || absValOne + 1 === absValTwo || absValOne - 1 === absValTwo);
};
export const scrollbarsHidingPluginName = '__osScrollbarsHidingPlugin';
export const scrollbarsHidingPlugin: Plugin<ScrollbarsHidingPluginInstance> = {
[scrollbarsHidingPluginName]: {
_createUniqueViewportArrangeElement: () => {
_createUniqueViewportArrangeElement: (env: InternalEnvironment) => {
const {
_nativeScrollbarsHiding: _nativeScrollbarStyling,
_nativeScrollbarsOverlaid: _nativeScrollbarIsOverlaid,
_cssCustomProperties,
} = getEnvironment();
} = env;
const create =
!_cssCustomProperties &&
!_nativeScrollbarStyling &&
@@ -67,14 +102,13 @@ export const scrollbarsHidingPlugin: Plugin<ScrollbarsHidingPluginInstance> = {
},
_overflowUpdateSegment: (
doViewportArrange,
flexboxGlue,
viewport,
viewportArrange,
getState,
getViewportOverflowState,
hideNativeScrollbars
) => {
const { _flexboxGlue } = getEnvironment();
/**
* Sets the styles of the viewport arrange element.
* @param viewportOverflowState The viewport overflow state according to which the scrollbars shall be hidden.
@@ -182,7 +216,7 @@ export const scrollbarsHidingPlugin: Plugin<ScrollbarsHidingPluginInstance> = {
removeClass(viewport, classNameViewportArrange);
if (!_flexboxGlue) {
if (!flexboxGlue) {
finalPaddingStyle.height = '';
}
@@ -207,5 +241,46 @@ export const scrollbarsHidingPlugin: Plugin<ScrollbarsHidingPluginInstance> = {
return [arrangeViewport, undoViewportArrange];
},
_envWindowZoom: () => {
let size = windowSize();
let dpr = getWindowDPR();
return (envInstance, updateNativeScrollbarSizeCache, triggerEvent) => {
const sizeNew = windowSize();
const deltaSize = {
w: sizeNew.w - size.w,
h: sizeNew.h - size.h,
};
if (deltaSize.w === 0 && deltaSize.h === 0) return;
const deltaAbsSize = {
w: abs(deltaSize.w),
h: abs(deltaSize.h),
};
const deltaAbsRatio = {
w: abs(round(sizeNew.w / (size.w / 100.0))),
h: abs(round(sizeNew.h / (size.h / 100.0))),
};
const dprNew = getWindowDPR();
const deltaIsBigger = deltaAbsSize.w > 2 && deltaAbsSize.h > 2;
const difference = !diffBiggerThanOne(deltaAbsRatio.w, deltaAbsRatio.h);
const dprChanged = dprNew !== dpr && dpr > 0;
const isZoom = deltaIsBigger && difference && dprChanged;
if (isZoom) {
const [scrollbarSize, scrollbarSizeChanged] = updateNativeScrollbarSizeCache();
assignDeep(envInstance._nativeScrollbarsSize, scrollbarSize); // keep the object same!
if (scrollbarSizeChanged) {
triggerEvent();
}
}
size = sizeNew;
dpr = dprNew;
};
},
},
};
@@ -6,12 +6,12 @@ import {
scrollLeft,
scrollTop,
on,
stopAndPrevent,
addClass,
equalWH,
push,
cAF,
rAF,
stopPropagation,
} from 'support';
import {
classNameSizeObserverListenerScroll,
@@ -68,7 +68,7 @@ export const sizeObserverPlugin: Plugin<SizeObserverPluginInstance> = {
isDirty = !scrollEvent || !equalWH(currSize, cacheSize);
if (scrollEvent) {
stopAndPrevent(scrollEvent);
stopPropagation(scrollEvent);
if (isDirty && !rAFId) {
cAF!(rAFId);
@@ -9,6 +9,7 @@ import {
removeClass,
removeElements,
runEachAndClear,
setT,
stopPropagation,
} from 'support';
import {
@@ -143,7 +144,7 @@ export const createScrollbarsSetupElements = (
appendChildren(evaluatedScrollbarSlot, horizontalScrollbars[0]._scrollbar);
appendChildren(evaluatedScrollbarSlot, verticalScrollbars[0]._scrollbar);
setTimeout(() => {
setT(() => {
addRemoveClassHorizontal(classNamesScrollbarTransitionless);
addRemoveClassVertical(classNamesScrollbarTransitionless);
}, 300);
@@ -1,4 +1,4 @@
import { rAF, cAF, isFunction, on, runEachAndClear } from 'support';
import { rAF, cAF, isFunction, on, runEachAndClear, setT, clearT } from 'support';
import { createState, createOptionCheck } from 'setups/setups';
import {
createScrollbarsSetupElements,
@@ -29,15 +29,15 @@ export interface ScrollbarsSetupStaticState {
const createSelfCancelTimeout = (timeout?: number | (() => number)) => {
let id: number;
const setT = timeout ? (setTimeout as (...args: any[]) => number) : rAF!;
const clearT = timeout ? clearTimeout : cAF!;
const setTFn = timeout ? setT : rAF!;
const clearTFn = timeout ? clearT : cAF!;
return [
(callback: () => any) => {
clearT(id);
clearTFn(id);
// @ts-ignore
id = setT(callback, isFunction(timeout) ? timeout() : timeout);
id = setTFn(callback, isFunction(timeout) ? timeout() : timeout);
},
() => clearT(id),
() => clearTFn(id),
] as [timeout: (callback: () => any) => void, clear: () => void];
};
@@ -85,7 +85,8 @@ const addDataAttrHost = (elm: HTMLElement, value: string) => {
export const createStructureSetupElements = (
target: InitializationTarget
): StructureSetupElements => {
const { _getInitializationStrategy, _nativeScrollbarsHiding } = getEnvironment();
const env = getEnvironment();
const { _getInitializationStrategy, _nativeScrollbarsHiding } = env;
const scrollbarsHidingPlugin = getPlugins()[scrollbarsHidingPluginName] as
| ScrollbarsHidingPluginInstance
| undefined;
@@ -156,7 +157,7 @@ export const createStructureSetupElements = (
!viewportIsTarget &&
!_nativeScrollbarsHiding &&
createUniqueViewportArrangeElement &&
createUniqueViewportArrangeElement(),
createUniqueViewportArrangeElement(env),
_windowElm: wnd,
_documentElm: ownerDocument,
_htmlElm: parent(bodyElm) as HTMLHtmlElement,
@@ -308,6 +308,7 @@ export const createOverflowUpdateSegment: CreateStructureUpdateSegment = (
const [arrangeViewport, undoViewportArrange] = scrollbarsHidingPlugin
? scrollbarsHidingPlugin._overflowUpdateSegment(
doViewportArrange,
_flexboxGlue,
_viewport,
_viewportArrange,
getState,
@@ -1,9 +1,10 @@
import { jsAPI } from 'support/compatibility/vendors';
export const MutationObserverConstructor = jsAPI<typeof MutationObserver>('MutationObserver');
export const IntersectionObserverConstructor = jsAPI<typeof IntersectionObserver>(
'IntersectionObserver'
);
export const IntersectionObserverConstructor =
jsAPI<typeof IntersectionObserver>('IntersectionObserver');
export const ResizeObserverConstructor = jsAPI<typeof ResizeObserver>('ResizeObserver');
export const cAF = jsAPI<typeof cancelAnimationFrame>('cancelAnimationFrame');
export const rAF = jsAPI<typeof requestAnimationFrame>('requestAnimationFrame');
export const setT = window.setTimeout as (handler: TimerHandler, timeout?: number) => number;
export const clearT = window.clearTimeout as (id?: number) => void;
@@ -1,11 +1,6 @@
import { isNumber, isFunction } from 'support/utils/types';
import { from } from 'support/utils/array';
import { rAF, cAF } from 'support/compatibility/apis';
const clearTimeouts = (id: number | undefined) => {
id && clearTimeout(id);
id && cAF!(id);
};
import { rAF, cAF, setT, clearT } from 'support/compatibility/apis';
type DebounceTiming = number | false | null | undefined;
@@ -44,17 +39,17 @@ export const debounce = <FunctionToDebounce extends (...args: any) => any>(
functionToDebounce: FunctionToDebounce,
options?: DebounceOptions<FunctionToDebounce>
): Debounced<FunctionToDebounce> => {
let timeoutId: number | undefined;
let maxTimeoutId: number | undefined;
let prevArguments: Parameters<FunctionToDebounce> | null | undefined;
let latestArguments: Parameters<FunctionToDebounce> | null | undefined;
let clear: () => void = noop;
const { _timeout, _maxDelay, _mergeParams } = options || {};
const setT = setTimeout as (...args: any[]) => number;
const invokeFunctionToDebounce = function (args: IArguments) {
clearTimeouts(timeoutId);
clearTimeouts(maxTimeoutId);
maxTimeoutId = timeoutId = prevArguments = undefined;
clear();
clearT(maxTimeoutId);
maxTimeoutId = prevArguments = undefined;
clear = noop;
// eslint-disable-next-line
// @ts-ignore
functionToDebounce.apply(this, args);
@@ -67,7 +62,7 @@ export const debounce = <FunctionToDebounce extends (...args: any) => any>(
const flush = () => {
/* istanbul ignore next */
if (timeoutId) {
if (clear !== noop) {
invokeFunctionToDebounce(mergeParms(latestArguments!) || latestArguments!);
}
};
@@ -82,6 +77,7 @@ export const debounce = <FunctionToDebounce extends (...args: any) => any>(
const finalMaxWait = isFunction(_maxDelay) ? _maxDelay() : _maxDelay;
const hasMaxWait = isNumber(finalMaxWait) && finalMaxWait >= 0;
const setTimeoutFn = finalTimeout > 0 ? setT : rAF!;
const clearTimeoutFn = finalTimeout > 0 ? clearT : cAF!;
const mergeParamsResult = mergeParms(args);
const invokedArgs = mergeParamsResult || args;
const boundInvoke = invokeFunctionToDebounce.bind(0, invokedArgs);
@@ -90,9 +86,10 @@ export const debounce = <FunctionToDebounce extends (...args: any) => any>(
// invokeFunctionToDebounce(prevArguments || args);
// }
clearTimeouts(timeoutId);
clear();
// @ts-ignore
timeoutId = setTimeoutFn(boundInvoke, finalTimeout as number) as number;
const timeoutId = setTimeoutFn(boundInvoke, finalTimeout);
clear = () => clearTimeoutFn(timeoutId);
if (hasMaxWait && !maxTimeoutId) {
maxTimeoutId = setT(flush, finalMaxWait as number);
@@ -1,26 +1,15 @@
import { noop, debounce } from 'support/utils/function';
import { rAF } from 'support/compatibility/apis';
import { rAF, setT } from 'support/compatibility/apis';
jest.mock('support/compatibility/apis', () => {
const originalModule = jest.requireActual('support/compatibility/apis');
return {
...originalModule,
rAF: jest.fn().mockImplementation((...args) => originalModule.rAF(...args)),
setT: jest.fn().mockImplementation((...args) => originalModule.setT(...args)),
};
});
const mockSetTimeout = () => {
const original = window.setTimeout;
// @ts-ignore
const setT = (window.setTimeout = jest.fn((...args) => original(...args)));
return [
setT,
() => {
window.setTimeout = original;
},
];
};
// eslint-disable-next-line no-return-await
const timeout = async (timeout = 100) => await new Promise((r) => setTimeout(r, timeout));
@@ -34,7 +23,6 @@ describe('function', () => {
describe('timeout', () => {
test('without timeout', () => {
let i = 0;
const [setT, unmockSetTimeout] = mockSetTimeout();
const debouncedFn = debounce(() => {
i += 1;
});
@@ -44,12 +32,10 @@ describe('function', () => {
expect(rAF).not.toHaveBeenCalled();
expect(setT).not.toHaveBeenCalled();
expect(i).toBe(1);
unmockSetTimeout();
});
test('with timeout 0', async () => {
let i = 0;
const [setT, unmockSetTimeout] = mockSetTimeout();
const debouncedFn = debounce(
() => {
i += 1;
@@ -67,12 +53,10 @@ describe('function', () => {
await timeout();
expect(i).toBe(1);
unmockSetTimeout();
});
test('with timeout > 0', async () => {
let i = 0;
const [setT, unmockSetTimeout] = mockSetTimeout();
const debouncedFn = debounce(
() => {
i += 1;
@@ -89,8 +73,6 @@ describe('function', () => {
expect(i).toBe(0);
await timeout();
expect(i).toBe(1);
unmockSetTimeout();
});
test('with timeout > 0 and multiple calls', async () => {
@@ -30,6 +30,7 @@ if (!window.ResizeObserver) {
addPlugin(sizeObserverPlugin);
}
if (!OverlayScrollbars.env().scrollbarsHiding) {
console.log('added');
addPlugin(scrollbarsHidingPlugin);
}
@@ -104,8 +104,6 @@ body {
background: blue;
border: 1px solid black;
padding: 10px;
height: 10000px;
width: 10000px;
}
.percent {
@@ -1 +0,0 @@
export {};