diff --git a/packages/overlayscrollbars/src/environment/environment.ts b/packages/overlayscrollbars/src/environment/environment.ts index 963c17b..88b1a53 100644 --- a/packages/overlayscrollbars/src/environment/environment.ts +++ b/packages/overlayscrollbars/src/environment/environment.ts @@ -7,19 +7,29 @@ import { absoluteCoordinates, offsetSize, scrollLeft, - jsAPI, XY, removeAttr, removeElements, windowSize, + runEach, } from 'support'; -type OnEnvironmentChanged = (env: Environment) => void; +export type OnEnvironmentChanged = (env: Environment) => void; +export interface Environment { + _autoUpdateLoop: boolean; + _nativeScrollbarSize: XY; + _nativeScrollbarIsOverlaid: XY; + _nativeScrollbarStyling: boolean; + _rtlScrollBehavior: { n: boolean; i: boolean }; + _addListener(listener: OnEnvironmentChanged): void; + _removeListener(listener: OnEnvironmentChanged): void; +} +let environmentInstance: Environment; const { abs, round } = Math; -const envornmentElmId = 'os-envornment'; +const environmentElmId = 'os-environment'; -const nativeScrollbarSize = (body: HTMLElement, measureElm: HTMLElement): XY => { +const getNativeScrollbarSize = (body: HTMLElement, measureElm: HTMLElement): XY => { appendChildren(body, measureElm); const cSize = clientSize(measureElm); const oSize = offsetSize(measureElm); @@ -30,7 +40,7 @@ const nativeScrollbarSize = (body: HTMLElement, measureElm: HTMLElement): XY => }; }; -const nativeScrollbarStyling = (testElm: HTMLElement): boolean => { +const getNativeScrollbarStyling = (testElm: HTMLElement): boolean => { let result = false; addClass(testElm, 'os-viewport-native-scrollbars-invisible'); try { @@ -41,7 +51,7 @@ const nativeScrollbarStyling = (testElm: HTMLElement): boolean => { return result; }; -const rtlScrollBehavior = (parentElm: HTMLElement, childElm: HTMLElement): { i: boolean; n: boolean } => { +const getRtlScrollBehavior = (parentElm: HTMLElement, childElm: HTMLElement): { i: boolean; n: boolean } => { const strHidden = 'hidden'; style(parentElm, { overflowX: strHidden, overflowY: strHidden, direction: 'rtl' }); scrollLeft(parentElm, 0); @@ -68,26 +78,7 @@ const rtlScrollBehavior = (parentElm: HTMLElement, childElm: HTMLElement): { i: }; }; -const passiveEvents = (): boolean => { - let supportsPassive = false; - try { - /* eslint-disable */ - // @ts-ignore - window.addEventListener( - 'test', - null, - Object.defineProperty({}, 'passive', { - get: function () { - supportsPassive = true; - }, - }) - ); - /* eslint-enable */ - } catch (e) {} - return supportsPassive; -}; - -const windowDPR = (): number => { +const getWindowDPR = (): number => { // eslint-disable-next-line // @ts-ignore const dDPI = window.screen.deviceXDPI || 0; @@ -103,100 +94,88 @@ const diffBiggerThanOne = (valOne: number, valTwo: number): boolean => { return !(absValOne === absValTwo || absValOne + 1 === absValTwo || absValOne - 1 === absValTwo); }; -export class Environment { - #onChangedListener: Set = new Set(); +const createEnvironment = (): Environment => { + const { body } = document; + const envDOM = createDOM(`
`); + const envElm = envDOM[0] as HTMLElement; + const envChildElm = envElm.firstChild as HTMLElement; - _autoUpdateLoop!: boolean; + const onChangedListener: Set = new Set(); + const nativeScrollBarSize = getNativeScrollbarSize(body, envElm); + const nativeScrollbarIsOverlaid = { + x: nativeScrollBarSize.x === 0, + y: nativeScrollBarSize.y === 0, + }; - _nativeScrollbarSize!: XY; + const env: Environment = { + _autoUpdateLoop: false, + _nativeScrollbarSize: nativeScrollBarSize, + _nativeScrollbarIsOverlaid: nativeScrollbarIsOverlaid, + _nativeScrollbarStyling: getNativeScrollbarStyling(envElm), + _rtlScrollBehavior: getRtlScrollBehavior(envElm, envChildElm), + _addListener(listener: OnEnvironmentChanged): void { + onChangedListener.add(listener); + }, + _removeListener(listener: OnEnvironmentChanged): void { + onChangedListener.delete(listener); + }, + }; - _nativeScrollbarIsOverlaid!: XY; + removeAttr(envElm, 'style'); + removeElements(envElm); - _nativeScrollbarStyling!: boolean; + if (!nativeScrollbarIsOverlaid.x || !nativeScrollbarIsOverlaid.y) { + let size = windowSize(); + let dpr = getWindowDPR(); + let scrollbarSize = nativeScrollBarSize; - _rtlScrollBehavior!: { n: boolean; i: boolean }; + window.addEventListener('resize', () => { + if (onChangedListener.size) { + const sizeNew = windowSize(); + const deltaSize = { + w: sizeNew.w - size.w, + h: sizeNew.h - size.h, + }; - _supportPassiveEvents!: boolean; + if (deltaSize.w === 0 && deltaSize.h === 0) return; - _supportResizeObserver!: boolean; + 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; - constructor() { - const _self = this; - const { body } = document; - const envDOM = createDOM(`
`); - const envElm = envDOM[0] as HTMLElement; - const envChildElm = envElm.firstChild as HTMLElement; + if (isZoom) { + const newScrollbarSize = (environmentInstance._nativeScrollbarSize = getNativeScrollbarSize(body, envElm)); + removeElements(envElm); - const nScrollBarSize = nativeScrollbarSize(body, envElm); - const nativeScrollbarIsOverlaid = { - x: nScrollBarSize.x === 0, - y: nScrollBarSize.y === 0, - }; - - _self._autoUpdateLoop = false; - _self._nativeScrollbarSize = nScrollBarSize; - _self._nativeScrollbarIsOverlaid = nativeScrollbarIsOverlaid; - _self._nativeScrollbarStyling = nativeScrollbarStyling(envElm); - _self._rtlScrollBehavior = rtlScrollBehavior(envElm, envChildElm); - _self._supportPassiveEvents = passiveEvents(); - _self._supportResizeObserver = !!jsAPI('ResizeObserver'); - - removeAttr(envElm, 'style'); - removeElements(envElm); - - if (!nativeScrollbarIsOverlaid.x || !nativeScrollbarIsOverlaid.y) { - let size = windowSize(); - let dpr = windowDPR(); - const onChangedListener = this.#onChangedListener; - - window.addEventListener('resize', () => { - if (onChangedListener.size) { - 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 = windowDPR(); - 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; - - const oldScrollbarSize = _self._nativeScrollbarSize; - let newScrollbarSize; - - if (isZoom) { - newScrollbarSize = _self._nativeScrollbarSize = nativeScrollbarSize(body, envElm); - removeElements(envElm); - - if (oldScrollbarSize.x !== newScrollbarSize.x || oldScrollbarSize.y !== newScrollbarSize.y) { - onChangedListener.forEach((listener) => listener && listener(_self)); - } + if (scrollbarSize.x !== newScrollbarSize.x || scrollbarSize.y !== newScrollbarSize.y) { + runEach(onChangedListener); } - size = sizeNew; - dpr = dprNew; + scrollbarSize = newScrollbarSize; } - }); - } + + size = sizeNew; + dpr = dprNew; + } + }); } - addListener(listener: OnEnvironmentChanged): void { - this.#onChangedListener.add(listener); - } + return env; +}; - removeListener(listener: OnEnvironmentChanged): void { - this.#onChangedListener.delete(listener); +export const getEnvironment = (): Environment => { + if (!environmentInstance) { + environmentInstance = createEnvironment(); } -} + return environmentInstance; +}; diff --git a/packages/overlayscrollbars/src/overlayscrollbars.scss b/packages/overlayscrollbars/src/overlayscrollbars.scss index fc5929b..eae946d 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars.scss +++ b/packages/overlayscrollbars/src/overlayscrollbars.scss @@ -1,6 +1,6 @@ @import './sizeobserver.scss'; -#os-envornment { +#os-environment { position: fixed; opacity: 0; visibility: hidden; @@ -8,14 +8,14 @@ height: 500px; width: 500px; } -#os-envornment > div { +#os-environment > div { width: 200%; height: 200%; margin: 10px 0; } /* fix restricted measuring */ -#os-envornment:before, -#os-envornment:after, +#os-environment:before, +#os-environment:after, .os-content:before, .os-content:after { content: ''; @@ -28,17 +28,17 @@ flex-shrink: 0; visibility: hidden; } -#os-envornment, +#os-environment, .os-viewport { -ms-overflow-style: scrollbar !important; } -.os-viewport-native-scrollbars-invisible#os-envornment, +.os-viewport-native-scrollbars-invisible#os-environment, .os-viewport-native-scrollbars-invisible.os-viewport { scrollbar-width: none !important; } -.os-viewport-native-scrollbars-invisible#os-envornment::-webkit-scrollbar, +.os-viewport-native-scrollbars-invisible#os-environment::-webkit-scrollbar, .os-viewport-native-scrollbars-invisible.os-viewport::-webkit-scrollbar, -.os-viewport-native-scrollbars-invisible#os-envornment::-webkit-scrollbar-corner, +.os-viewport-native-scrollbars-invisible#os-environment::-webkit-scrollbar-corner, .os-viewport-native-scrollbars-invisible.os-viewport::-webkit-scrollbar-corner { display: none !important; width: 0px !important; diff --git a/packages/overlayscrollbars/src/overlayscrollbars/observers/SizeObserver.ts b/packages/overlayscrollbars/src/overlayscrollbars/observers/SizeObserver.ts index a93b377..d45e3e7 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars/observers/SizeObserver.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars/observers/SizeObserver.ts @@ -13,7 +13,7 @@ import { preventDefault, stopPropagation, } from 'support'; -import { Environment } from 'environment'; +import { getEnvironment } from 'environment'; const animationStartEventName = 'animationstart'; const scrollEventName = 'scroll'; @@ -29,15 +29,9 @@ const getDirection = (elm: HTMLElement) => style(elm, 'direction'); // TODO: // 1. MAYBE add comparison function to offsetSize etc. -// 2. remove supportPassiveListeners & resizeobserver from environment -export const createSizeObserver = ( - target: HTMLElement, - onSizeChangedCallback: (direction?: boolean) => any, - environment: Environment, - direction?: boolean -) => { - const rtlScrollBehavior = environment._rtlScrollBehavior; +export const createSizeObserver = (target: HTMLElement, onSizeChangedCallback: (direction?: boolean) => any, direction?: boolean) => { + const rtlScrollBehavior = getEnvironment()._rtlScrollBehavior; const baseElements = createDOM(`
`); const sizeObserver = baseElements[0] as HTMLElement; const listenerElement = sizeObserver.firstChild as HTMLElement; diff --git a/packages/overlayscrollbars/src/overlayscrollbars/observers/TrinsicObserver.ts b/packages/overlayscrollbars/src/overlayscrollbars/observers/TrinsicObserver.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/overlayscrollbars/src/support/utils/array.ts b/packages/overlayscrollbars/src/support/utils/array.ts index 9775a0e..e592ca7 100644 --- a/packages/overlayscrollbars/src/support/utils/array.ts +++ b/packages/overlayscrollbars/src/support/utils/array.ts @@ -1,6 +1,8 @@ import { isArrayLike } from 'support/utils/types'; import { PlainObject } from 'typings'; +type RunEachItem = ((...args: any) => any | any[]) | null | undefined; + /** * Iterates through a array or object * @param arrayLikeOrObject The array or object through which shall be iterated. @@ -28,7 +30,7 @@ export function each(obj: PlainObject, callback: (value: any, indexOrKey: string export function each(obj: PlainObject | null, callback: (value: any, indexOrKey: string, source: PlainObject) => boolean | void): PlainObject | null; export function each( source: ArrayLike | PlainObject | null, - callback: (value: T | any, indexOrKey: any, source: any) => boolean | void + callback: (value: T, indexOrKey: any, source: any) => boolean | void ): Array | ReadonlyArray | ArrayLike | PlainObject | null { if (isArrayLike(source)) { for (let i = 0; i < source.length; i++) { @@ -66,9 +68,13 @@ export const from = (arr: ArrayLike) => { }; /** - * Calls all functions in the passed array of functions. + * Calls all functions in the passed array/set of functions. * @param arr The array filled with function which shall be called. */ -export const runEach = (arr: Array<((...args: any) => any | any[]) | null | undefined>): void => { - each(arr, (fn) => fn && fn()); +export const runEach = (arr: ArrayLike | Set): void => { + if (arr instanceof Set) { + arr.forEach((fn) => fn && fn()); + } else { + each(arr, (fn) => fn && fn()); + } }; diff --git a/packages/overlayscrollbars/tests/jsdom/support/utils/arrays.test.ts b/packages/overlayscrollbars/tests/jsdom/support/utils/arrays.test.ts index 00cd8f2..3a21ffc 100644 --- a/packages/overlayscrollbars/tests/jsdom/support/utils/arrays.test.ts +++ b/packages/overlayscrollbars/tests/jsdom/support/utils/arrays.test.ts @@ -192,18 +192,30 @@ describe('array utilities', () => { }); }); + describe('runEach', () => { + test('array', () => { + const arr = [jest.fn(), null, jest.fn(), undefined, jest.fn()]; + runEach(arr); + arr.forEach((fn) => { + if (fn) { + expect(fn).toHaveBeenCalled(); + } + }); + }); + + test('set', () => { + const set = new Set([jest.fn(), null, jest.fn(), undefined, jest.fn()]); + runEach(set); + set.forEach((fn) => { + if (fn) { + expect(fn).toHaveBeenCalled(); + } + }); + }); + }); + test('indexOf', () => { const idx = indexOf([1, 2, 3], 2); expect(idx).toBe(1); }); - - test('runEach', () => { - const arr = [jest.fn(), null, jest.fn(), undefined, jest.fn()]; - runEach(arr); - arr.forEach((fn) => { - if (fn) { - expect(fn).toHaveBeenCalled(); - } - }); - }); }); diff --git a/packages/overlayscrollbars/tests/puppeteer/Environment/index.browser.ts b/packages/overlayscrollbars/tests/puppeteer/Environment/index.browser.ts index ce2b25c..0aa1898 100644 --- a/packages/overlayscrollbars/tests/puppeteer/Environment/index.browser.ts +++ b/packages/overlayscrollbars/tests/puppeteer/Environment/index.browser.ts @@ -1,7 +1,7 @@ import 'overlayscrollbars.scss'; -import { Environment } from 'environment'; +import { getEnvironment } from 'environment'; -const envInstance = new Environment(); +const envInstance = getEnvironment(); document.body.textContent = JSON.stringify(envInstance); export { envInstance }; diff --git a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts index 651b18a..c0f1b1c 100644 --- a/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts +++ b/packages/overlayscrollbars/tests/puppeteer/SizeObserver/index.browser.ts @@ -6,7 +6,6 @@ import { generateSelectCallback, iterateSelect } from '@/testing-browser/Select' import { setTestResult } from '@/testing-browser/TestResult'; import { hasDimensions, offsetSize, WH, style } from 'support'; -import { Environment } from 'environment'; import { createSizeObserver } from 'overlayscrollbars/observers/SizeObserver'; let sizeIterations = 0; @@ -171,7 +170,6 @@ createSizeObserver( } }); }, - new Environment(), true );