diff --git a/packages/overlayscrollbars/src/environment.ts b/packages/overlayscrollbars/src/environment.ts index d97ea8e..86a27a6 100644 --- a/packages/overlayscrollbars/src/environment.ts +++ b/packages/overlayscrollbars/src/environment.ts @@ -16,6 +16,8 @@ import { getBoundingClientRect, assignDeep, cssProperty, + createCache, + equalXY, } from 'support'; import { classNameEnvironment, @@ -26,59 +28,72 @@ import { import { OSOptions, defaultOptions } from 'options'; import { OSTargetElement, PartialOptions } from 'typings'; -type StructureInitializationElementFn = ((target: OSTargetElement) => HTMLElement | T) | T; +type StructureInitializationStrategyElementFn = + | ((target: OSTargetElement) => HTMLElement | T) + | T; -type ScrollbarsInitializationElementFn = +type ScrollbarsInitializationStrategyElementFn = | ((target: OSTargetElement, host: HTMLElement, viewport: HTMLElement) => HTMLElement | T) | T; /** * A Static element is an element which MUST be generated. - * If null (or the returned result is null), the initialization function is generatig the element, otherwise + * If null or undefined (or the returned result is null or undefined), the initialization function is generatig the element, otherwise * the element returned by the function acts as the generated element. */ -export type StructureInitializationStaticElement = StructureInitializationElementFn; +export type StructureInitializationStrategyStaticElement = StructureInitializationStrategyElementFn< + null | undefined +>; /** * A Dynamic element is an element which CAN be generated. - * If null (or the returned result is null), then the default behavior is used. * If boolean (or the returned result is boolean), the generation of the element is forced (or not). * If the function returns and element, the element returned by the function acts as the generated element. */ -export type StructureInitializationDynamicElement = StructureInitializationElementFn< - boolean | null ->; +export type StructureInitializationStrategyDynamicElement = + StructureInitializationStrategyElementFn; export interface StructureInitializationStrategy { - _host: StructureInitializationStaticElement; - _viewport: StructureInitializationStaticElement; - _padding: StructureInitializationDynamicElement; - _content: StructureInitializationDynamicElement; + _host: StructureInitializationStrategyStaticElement; + _viewport: StructureInitializationStrategyStaticElement; + _padding: StructureInitializationStrategyDynamicElement; + _content: StructureInitializationStrategyDynamicElement; } export interface ScrollbarsInitializationStrategy { - _scrollbarsSlot: ScrollbarsInitializationElementFn; + /** + * The scrollbars slot. If null or undefined (or the returned result is null or undefined), the initialization function is deciding the element, otherwise + * the element returned by the function acts as the scrollbars slot. + */ + _scrollbarsSlot: ScrollbarsInitializationStrategyElementFn; } export interface InitializationStrategy extends StructureInitializationStrategy, ScrollbarsInitializationStrategy {} +export type DefaultInitializationStrategy = { + [K in keyof InitializationStrategy]: Extract< + InitializationStrategy[K], + boolean | null | undefined + >; +}; + export type OnEnvironmentChanged = (env: Environment) => void; export interface Environment { - _nativeScrollbarSize: XY; - _nativeScrollbarIsOverlaid: XY; - _nativeScrollbarStyling: boolean; - _rtlScrollBehavior: { n: boolean; i: boolean }; - _flexboxGlue: boolean; - _cssCustomProperties: boolean; + readonly _nativeScrollbarSize: XY; + readonly _nativeScrollbarIsOverlaid: XY; + readonly _nativeScrollbarStyling: boolean; + readonly _rtlScrollBehavior: { n: boolean; i: boolean }; + readonly _flexboxGlue: boolean; + readonly _cssCustomProperties: boolean; + readonly _defaultInitializationStrategy: DefaultInitializationStrategy; + readonly _defaultDefaultOptions: OSOptions; _addListener(listener: OnEnvironmentChanged): () => void; _getInitializationStrategy(): InitializationStrategy; _setInitializationStrategy(newInitializationStrategy: Partial): void; _getDefaultOptions(): OSOptions; _setDefaultOptions(newDefaultOptions: PartialOptions): void; - _defaultInitializationStrategy: InitializationStrategy; - _defaultDefaultOptions: OSOptions; } let environmentInstance: Environment; @@ -168,14 +183,13 @@ const getWindowDPR = (): number => { return window.devicePixelRatio || dDPI / sDPI; }; -// init function decides for all values const getDefaultInitializationStrategy = ( nativeScrollbarStyling: boolean -): InitializationStrategy => ({ +): DefaultInitializationStrategy => ({ _host: null, _viewport: null, - _padding: null, - _content: null, + _padding: !nativeScrollbarStyling, + _content: false, _scrollbarsSlot: null, }); @@ -185,15 +199,18 @@ const createEnvironment = (): Environment => { const envElm = envDOM[0] as HTMLElement; const envChildElm = envElm.firstChild as HTMLElement; const onChangedListener: Set = new Set(); - const nativeScrollbarSize = getNativeScrollbarSize(body, envElm); + const [updateNativeScrollbarSizeCache, getNativeScrollbarSizeCache] = createCache({ + _initialValue: getNativeScrollbarSize(body, envElm), + _equal: equalXY, + }); + const [nativeScrollbarSize] = getNativeScrollbarSizeCache(); const nativeScrollbarStyling = getNativeScrollbarStyling(envElm); const nativeScrollbarIsOverlaid = { x: nativeScrollbarSize.x === 0, y: nativeScrollbarSize.y === 0, }; - const defaultInitializationStrategy = getDefaultInitializationStrategy(nativeScrollbarStyling); - let initializationStrategy = defaultInitializationStrategy; - let defaultDefaultOptions = defaultOptions; + const initializationStrategy = getDefaultInitializationStrategy(nativeScrollbarStyling); + const defaultDefaultOptions = assignDeep({}, defaultOptions); const env: Environment = { _nativeScrollbarSize: nativeScrollbarSize, @@ -206,16 +223,24 @@ const createEnvironment = (): Environment => { onChangedListener.add(listener); return () => onChangedListener.delete(listener); }, - _getInitializationStrategy: () => ({ ...initializationStrategy }), + _getInitializationStrategy: assignDeep.bind( + 0, + {} as InitializationStrategy, + initializationStrategy + ), _setInitializationStrategy(newInitializationStrategy) { - initializationStrategy = assignDeep({}, initializationStrategy, newInitializationStrategy); + assignDeep(initializationStrategy, newInitializationStrategy); }, - _getDefaultOptions: () => ({ ...defaultDefaultOptions }), + _getDefaultOptions: assignDeep.bind( + 0, + {} as OSOptions, + defaultDefaultOptions + ), _setDefaultOptions(newDefaultOptions) { - defaultDefaultOptions = assignDeep({}, defaultDefaultOptions, newDefaultOptions); + assignDeep(defaultDefaultOptions, newDefaultOptions); }, - _defaultInitializationStrategy: defaultInitializationStrategy, - _defaultDefaultOptions: defaultDefaultOptions, + _defaultInitializationStrategy: assignDeep({}, initializationStrategy), + _defaultDefaultOptions: assignDeep({}, defaultDefaultOptions), }; removeAttr(envElm, 'style'); @@ -224,7 +249,6 @@ const createEnvironment = (): Environment => { if (!nativeScrollbarStyling && (!nativeScrollbarIsOverlaid.x || !nativeScrollbarIsOverlaid.y)) { let size = windowSize(); let dpr = getWindowDPR(); - let scrollbarSize = nativeScrollbarSize; window.addEventListener('resize', () => { if (onChangedListener.size) { @@ -251,18 +275,16 @@ const createEnvironment = (): Environment => { const isZoom = deltaIsBigger && difference && dprChanged; if (isZoom) { - const newScrollbarSize = getNativeScrollbarSize(body, envElm); - // keep the object same! - environmentInstance._nativeScrollbarSize.x = newScrollbarSize.x; - environmentInstance._nativeScrollbarSize.y = newScrollbarSize.y; + const [scrollbarSize, scrollbarSizeChanged] = updateNativeScrollbarSizeCache( + getNativeScrollbarSize(body, envElm) + ); + assignDeep(environmentInstance._nativeScrollbarSize, scrollbarSize); // keep the object same! removeElements(envElm); - if (scrollbarSize.x !== newScrollbarSize.x || scrollbarSize.y !== newScrollbarSize.y) { + if (scrollbarSizeChanged) { runEach(onChangedListener); } - - scrollbarSize = newScrollbarSize; } size = sizeNew; diff --git a/packages/overlayscrollbars/src/options.ts b/packages/overlayscrollbars/src/options.ts index ad260cc..345f733 100644 --- a/packages/overlayscrollbars/src/options.ts +++ b/packages/overlayscrollbars/src/options.ts @@ -1,5 +1,5 @@ import { assignDeep, each, isObject, keys, isArray, hasOwnProperty, isFunction } from 'support'; -import { PartialOptions } from 'typings'; +import { PartialOptions, ReadonlyOptions } from 'typings'; const stringify = (value: any) => JSON.stringify(value, (_, val) => { @@ -83,6 +83,8 @@ export interface OSOptions { */ } +export type ReadonlyOSOptions = ReadonlyOptions; + export interface OverflowChangedArgs { x: boolean; y: boolean; diff --git a/packages/overlayscrollbars/src/overlayscrollbars.ts b/packages/overlayscrollbars/src/overlayscrollbars.ts index 42ce05e..c4f5e26 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars.ts @@ -1,7 +1,7 @@ import { OSTarget, OSInitializationObject, PartialOptions } from 'typings'; import { assignDeep, isEmptyObject, each, isFunction, keys, isHTMLElement, WH, XY } from 'support'; import { createStructureSetup, createScrollbarsSetup } from 'setups'; -import { getOptionsDiff, OSOptions } from 'options'; +import { getOptionsDiff, OSOptions, ReadonlyOSOptions } from 'options'; import { getEnvironment } from 'environment'; import { getPlugins, @@ -78,7 +78,11 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = ( const validate = optionsValidationPlugin && optionsValidationPlugin._; return validate ? validate(opts, true) : opts; }; - const currentOptions: OSOptions = assignDeep({}, _getDefaultOptions(), validateOptions(options)); + const currentOptions: ReadonlyOSOptions = assignDeep( + {}, + _getDefaultOptions(), + validateOptions(options) + ); const [addEvent, removeEvent, triggerEvent] = createOSEventListenerHub(eventListeners); if ( diff --git a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts index 218c834..7382b41 100644 --- a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts +++ b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts @@ -4,7 +4,7 @@ import { ScrollbarsSetupElementsObj, } from 'setups/scrollbarsSetup/scrollbarsSetup.elements'; import type { StructureSetupElementsObj } from 'setups/structureSetup/structureSetup.elements'; -import type { OSOptions } from 'options'; +import type { ReadonlyOSOptions } from 'options'; import type { Setup } from 'setups'; import type { OSTarget } from 'typings'; @@ -17,7 +17,7 @@ export interface ScrollbarsSetupStaticState { export const createScrollbarsSetup = ( target: OSTarget, - options: OSOptions, + options: ReadonlyOSOptions, structureSetupElements: StructureSetupElementsObj ): Setup => { const state = createState({}); diff --git a/packages/overlayscrollbars/src/setups/setups.ts b/packages/overlayscrollbars/src/setups/setups.ts index 9b1de4f..633c0ce 100644 --- a/packages/overlayscrollbars/src/setups/setups.ts +++ b/packages/overlayscrollbars/src/setups/setups.ts @@ -1,5 +1,5 @@ import { assignDeep, hasOwnProperty } from 'support'; -import type { OSOptions } from 'options'; +import type { OSOptions, ReadonlyOSOptions } from 'options'; import type { PartialOptions } from 'typings'; export type SetupElements> = [elements: T, destroy: () => void]; @@ -35,7 +35,7 @@ const getPropByPath = (obj: any, path: string): T => export const createOptionCheck = ( - options: OSOptions, + options: ReadonlyOSOptions, changedOptions: PartialOptions, force?: boolean ): SetupUpdateCheckOption => diff --git a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.elements.ts b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.elements.ts index 15fb0fb..0c03996 100644 --- a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.elements.ts +++ b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.elements.ts @@ -28,9 +28,8 @@ import { } from 'classnames'; import { getEnvironment, - StructureInitializationStaticElement, - StructureInitializationDynamicElement, - StructureInitializationStrategy, + StructureInitializationStrategyStaticElement, + StructureInitializationStrategyDynamicElement, } from 'environment'; import { OSTarget, OSTargetElement, StructureInitialization } from 'typings'; @@ -81,20 +80,20 @@ const createUniqueViewportArrangeElement = (): HTMLStyleElement | false => { const staticCreationFromStrategy = ( target: OSTargetElement, initializationValue: HTMLElement | undefined, - strategy: StructureInitializationStaticElement, + strategy: StructureInitializationStrategyStaticElement, elementClass: string ): HTMLElement => { const result = - initializationValue || (isFunction(strategy) ? strategy(target) : (strategy as null)); + initializationValue || + (isFunction(strategy) ? strategy(target) : (strategy as null | undefined)); return result || createDiv(elementClass); }; const dynamicCreationFromStrategy = ( target: OSTargetElement, initializationValue: HTMLElement | boolean | undefined, - strategy: StructureInitializationDynamicElement, - elementClass: string, - defaultValue: boolean + strategy: StructureInitializationStrategyDynamicElement, + elementClass: string ): HTMLElement | false => { const takeInitializationValue = isBoolean(initializationValue) || initializationValue; const result = takeInitializationValue @@ -103,10 +102,6 @@ const dynamicCreationFromStrategy = ( ? strategy(target) : strategy; - if (result === null) { - return defaultValue ? createDiv(elementClass) : false; - } - return result === true ? createDiv(elementClass) : result; }; @@ -117,7 +112,7 @@ export const createStructureSetupElements = (target: OSTarget): StructureSetupEl _viewport: viewportInitializationStrategy, _padding: paddingInitializationStrategy, _content: contentInitializationStrategy, - } = _getInitializationStrategy() as StructureInitializationStrategy; + } = _getInitializationStrategy(); const targetIsElm = isHTMLElement(target); const targetStructureInitialization = target as StructureInitialization; const targetElement = targetIsElm @@ -125,7 +120,7 @@ export const createStructureSetupElements = (target: OSTarget): StructureSetupEl : targetStructureInitialization.target; const isTextarea = is(targetElement, 'textarea'); const isBody = !isTextarea && is(targetElement, 'body'); - const ownerDocument: HTMLDocument = targetElement!.ownerDocument; + const ownerDocument = targetElement!.ownerDocument; const bodyElm = ownerDocument.body as HTMLBodyElement; const wnd = ownerDocument.defaultView as Window; const evaluatedTargetObj: StructureSetupElementsObj = { @@ -148,15 +143,13 @@ export const createStructureSetupElements = (target: OSTarget): StructureSetupEl targetElement, targetStructureInitialization.padding, paddingInitializationStrategy, - classNamePadding, - !_nativeScrollbarStyling // default value for padding + classNamePadding ), _content: dynamicCreationFromStrategy( targetElement, targetStructureInitialization.content, contentInitializationStrategy, - classNameContent, - false // default value for content + classNameContent ), _viewportArrange: createUniqueViewportArrangeElement(), _windowElm: wnd, diff --git a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts index 7623827..219b365 100644 --- a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts +++ b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.ts @@ -6,7 +6,7 @@ import { createStructureSetupObservers } from 'setups/structureSetup/structureSe import type { StructureSetupUpdateHints } from 'setups/structureSetup/structureSetup.update'; import type { StructureSetupElementsObj } from 'setups/structureSetup/structureSetup.elements'; import type { TRBL, CacheValues, XY, WH } from 'support'; -import type { OSOptions } from 'options'; +import type { OSOptions, ReadonlyOSOptions } from 'options'; import type { Setup } from 'setups'; import type { OSTarget, PartialOptions, StyleObject } from 'typings'; @@ -64,7 +64,7 @@ const initialStructureSetupUpdateState: StructureSetupState = { export const createStructureSetup = ( target: OSTarget, - options: OSOptions + options: ReadonlyOSOptions ): Setup => { const checkOptionsFallback = createOptionCheck(options, {}); const state = createState(initialStructureSetupUpdateState); diff --git a/packages/overlayscrollbars/src/typings.ts b/packages/overlayscrollbars/src/typings.ts index d52315b..5f78a8b 100644 --- a/packages/overlayscrollbars/src/typings.ts +++ b/packages/overlayscrollbars/src/typings.ts @@ -2,6 +2,10 @@ export type PartialOptions = { [P in keyof T]?: T[P] extends Record ? PartialOptions : T[P]; }; +export type ReadonlyOptions = { + readonly [P in keyof T]: T[P] extends Record ? ReadonlyOptions : T[P]; +}; + export type PlainObject = { [name: string]: T }; export type StyleObject = { diff --git a/packages/overlayscrollbars/tests/jest/setups/structureSetup/structureSetup.elements.test.ts b/packages/overlayscrollbars/tests/jest/setups/structureSetup/structureSetup.elements.test.ts index 683ab13..0748028 100644 --- a/packages/overlayscrollbars/tests/jest/setups/structureSetup/structureSetup.elements.test.ts +++ b/packages/overlayscrollbars/tests/jest/setups/structureSetup/structureSetup.elements.test.ts @@ -1,7 +1,7 @@ import { Environment, - StructureInitializationStaticElement, - StructureInitializationDynamicElement, + StructureInitializationStrategyStaticElement, + StructureInitializationStrategyDynamicElement, } from 'environment'; import { OSTarget, StructureInitialization } from 'typings'; import { @@ -167,8 +167,9 @@ const assertCorrectSetupElements = ( elm: Element | null, input: HTMLElement | boolean | undefined, isStaticStrategy: boolean, - strategy: StructureInitializationStaticElement | StructureInitializationDynamicElement, - id: string + strategy: + | StructureInitializationStrategyStaticElement + | StructureInitializationStrategyDynamicElement ) => { if (input) { expect(elm).toBeTruthy(); @@ -178,7 +179,7 @@ const assertCorrectSetupElements = ( } if (input === undefined) { if (isStaticStrategy) { - strategy = strategy as StructureInitializationStaticElement; + strategy = strategy as StructureInitializationStrategyStaticElement; if (typeof strategy === 'function') { const result = strategy(target); if (result) { @@ -190,21 +191,14 @@ const assertCorrectSetupElements = ( expect(elm).toBeTruthy(); } } else { - strategy = strategy as StructureInitializationDynamicElement; - const expectDefaultValue = () => { - if (id === 'padding') { - if (_nativeScrollbarStyling) { - expect(elm).toBeFalsy(); - } else { - expect(elm).toBeTruthy(); - } - } else if (id === 'content') { - expect(elm).toBeFalsy(); - } - }; + strategy = strategy as StructureInitializationStrategyDynamicElement; + expect(strategy).not.toBe(null); + expect(strategy).not.toBe(undefined); if (typeof strategy === 'function') { const result = strategy(target); const resultIsBoolean = typeof result === 'boolean'; + expect(result).not.toBe(null); + expect(result).not.toBe(undefined); if (resultIsBoolean) { if (result) { expect(elm).toBeTruthy(); @@ -213,8 +207,6 @@ const assertCorrectSetupElements = ( } } else if (result) { expect(elm).toBe(result); - } else { - expectDefaultValue(); } } else { const strategyIsBoolean = typeof strategy === 'boolean'; @@ -224,8 +216,6 @@ const assertCorrectSetupElements = ( } else { expect(elm).toBeFalsy(); } - } else { - expectDefaultValue(); } } } @@ -240,10 +230,10 @@ const assertCorrectSetupElements = ( } if (inputIsElement) { - checkStrategyDependendElements(padding, undefined, false, paddingInitStrategy, 'padding'); - checkStrategyDependendElements(content, undefined, false, contentInitStrategy, 'content'); - checkStrategyDependendElements(viewport, undefined, true, viewportInitStrategy, 'viewport'); - checkStrategyDependendElements(host, undefined, true, hostInitStrategy, 'host'); + checkStrategyDependendElements(padding, undefined, false, paddingInitStrategy); + checkStrategyDependendElements(content, undefined, false, contentInitStrategy); + checkStrategyDependendElements(viewport, undefined, true, viewportInitStrategy); + checkStrategyDependendElements(host, undefined, true, hostInitStrategy); } else { const { padding: inputPadding, @@ -251,10 +241,10 @@ const assertCorrectSetupElements = ( viewport: inputViewport, host: inputHost, } = inputAsObj; - checkStrategyDependendElements(padding, inputPadding, false, paddingInitStrategy, 'padding'); - checkStrategyDependendElements(content, inputContent, false, contentInitStrategy, 'content'); - checkStrategyDependendElements(viewport, inputViewport, true, viewportInitStrategy, 'viewport'); - checkStrategyDependendElements(host, inputHost, true, hostInitStrategy, 'host'); + checkStrategyDependendElements(padding, inputPadding, false, paddingInitStrategy); + checkStrategyDependendElements(content, inputContent, false, contentInitStrategy); + checkStrategyDependendElements(viewport, inputViewport, true, viewportInitStrategy); + checkStrategyDependendElements(host, inputHost, true, hostInitStrategy); } return [elements, destroy]; @@ -327,8 +317,12 @@ const envInitStrategyAssigned = { _getInitializationStrategy: () => ({ _host: () => document.querySelector('#host1') as HTMLElement, _viewport: (target: HTMLElement) => target.querySelector('#viewport') as HTMLElement, - _content: (target: HTMLElement) => target.querySelector('#content') as HTMLElement, - _padding: (target: HTMLElement) => target.querySelector('#padding') as HTMLElement, + _content: (target: HTMLElement) => + target.querySelector('#content') || + env._defaultInitializationStrategy._content, + _padding: (target: HTMLElement) => + target.querySelector('#padding') || + env._defaultInitializationStrategy._padding, _scrollbarsSlot: null, }), },