diff --git a/packages/overlayscrollbars/src/environment/environment.ts b/packages/overlayscrollbars/src/environment/environment.ts index 80c283d..e956f7c 100644 --- a/packages/overlayscrollbars/src/environment/environment.ts +++ b/packages/overlayscrollbars/src/environment/environment.ts @@ -143,7 +143,7 @@ export class Environment { removeAttr(envElm, 'style'); removeElements(envElm); - if (nativeScrollbarIsOverlaid.x && nativeScrollbarIsOverlaid.y) { + if (!nativeScrollbarIsOverlaid.x || !nativeScrollbarIsOverlaid.y) { let size = windowSize(); let dpr = windowDPR(); const onChangedListener = this.#onChangedListener; diff --git a/packages/overlayscrollbars/src/support/utils/extend.ts b/packages/overlayscrollbars/src/support/utils/extend.ts deleted file mode 100644 index 406841d..0000000 --- a/packages/overlayscrollbars/src/support/utils/extend.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { isArray, isFunction, isPlainObject, isNull } from 'support/utils/types'; -import { each } from 'support/utils/array'; -import { keys } from 'support/utils/object'; - -// https://github.com/jquery/jquery/blob/master/src/core.js#L116 -export function extend(target: T, object1: U): T & U; -export function extend(target: T, object1: U, object2: V): T & U & V; -export function extend(target: T, object1: U, object2: V, object3: W): T & U & V & W; -export function extend(target: T, object1: U, object2: V, object3: W, object4: X): T & U & V & W & X; -export function extend(target: T, object1: U, object2: V, object3: W, object4: X, object5: Y): T & U & V & W & X & Y; -export function extend( - target: T, - object1?: U, - object2?: V, - object3?: W, - object4?: X, - object5?: Y, - object6?: Z -): T & U & V & W & X & Y & Z { - const sources: Array = [object1, object2, object3, object4, object5, object6]; - - // Handle case when target is a string or something (possible in deep copy) - if ((typeof target !== 'object' || isNull(target)) && !isFunction(target)) { - target = {} as T; - } - - each(sources, (source) => { - // Extend the base object - each(keys(source), (key) => { - const copy: any = source[key]; - - // Prevent Object.prototype pollution - // Prevent never-ending loop - if (target === copy) { - return true; - } - - const copyIsArray = isArray(copy); - - // Recurse if we're merging plain objects or arrays - if (copy && (isPlainObject(copy) || copyIsArray)) { - const src = target[key]; - let clone: any = src; - - // Ensure proper type for the source value - if (copyIsArray && !isArray(src)) { - clone = []; - } else if (!copyIsArray && !isPlainObject(src)) { - clone = {}; - } - - // Never move original objects, clone them - target[key] = extend(clone, copy) as any; - - // Don't bring in undefined values - } else if (copy !== undefined) { - target[key] = copy; - } - }); - }); - - // Return the modified object - return target as any; -} diff --git a/packages/overlayscrollbars/src/support/utils/index.ts b/packages/overlayscrollbars/src/support/utils/index.ts index e275e87..3a3a49c 100644 --- a/packages/overlayscrollbars/src/support/utils/index.ts +++ b/packages/overlayscrollbars/src/support/utils/index.ts @@ -1,4 +1,3 @@ export * from 'support/utils/array'; export * from 'support/utils/object'; -export * from 'support/utils/extend'; export * from 'support/utils/types'; diff --git a/packages/overlayscrollbars/src/support/utils/object.ts b/packages/overlayscrollbars/src/support/utils/object.ts index c52ea1a..292f59d 100644 --- a/packages/overlayscrollbars/src/support/utils/object.ts +++ b/packages/overlayscrollbars/src/support/utils/object.ts @@ -1,13 +1,76 @@ +import { isArray, isFunction, isPlainObject, isNull } from 'support/utils/types'; +import { each } from 'support/utils/array'; + /** * Determines whether the passed object has a property with the passed name. * @param obj The object. * @param prop The name of the property. */ -export const hasOwnProperty = (obj: any, prop: string | number | symbol): boolean => - Object.prototype.hasOwnProperty.call(obj, prop); +export const hasOwnProperty = (obj: any, prop: string | number | symbol): boolean => Object.prototype.hasOwnProperty.call(obj, prop); /** * Returns the names of the enumerable string properties and methods of an object. * @param obj The object of which the properties shall be returned. */ export const keys = (obj: any): Array => (obj ? Object.keys(obj) : []); + +// https://github.com/jquery/jquery/blob/master/src/core.js#L116 +export function extend(target: T, object1: U): T & U; +export function extend(target: T, object1: U, object2: V): T & U & V; +export function extend(target: T, object1: U, object2: V, object3: W): T & U & V & W; +export function extend(target: T, object1: U, object2: V, object3: W, object4: X): T & U & V & W & X; +export function extend(target: T, object1: U, object2: V, object3: W, object4: X, object5: Y): T & U & V & W & X & Y; +export function extend( + target: T, + object1?: U, + object2?: V, + object3?: W, + object4?: X, + object5?: Y, + object6?: Z +): T & U & V & W & X & Y & Z { + const sources: Array = [object1, object2, object3, object4, object5, object6]; + + // Handle case when target is a string or something (possible in deep copy) + if ((typeof target !== 'object' || isNull(target)) && !isFunction(target)) { + target = {} as T; + } + + each(sources, (source) => { + // Extend the base object + each(keys(source), (key) => { + const copy: any = source[key]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if (target === copy) { + return true; + } + + const copyIsArray = isArray(copy); + + // Recurse if we're merging plain objects or arrays + if (copy && (isPlainObject(copy) || copyIsArray)) { + const src = target[key]; + let clone: any = src; + + // Ensure proper type for the source value + if (copyIsArray && !isArray(src)) { + clone = []; + } else if (!copyIsArray && !isPlainObject(src)) { + clone = {}; + } + + // Never move original objects, clone them + target[key] = extend(clone, copy) as any; + + // Don't bring in undefined values + } else if (copy !== undefined) { + target[key] = copy; + } + }); + }); + + // Return the modified object + return target as any; +} diff --git a/packages/overlayscrollbars/tests/support/dom/dimensions.test.ts b/packages/overlayscrollbars/tests/support/dom/dimensions.test.ts new file mode 100644 index 0000000..b0078fc --- /dev/null +++ b/packages/overlayscrollbars/tests/support/dom/dimensions.test.ts @@ -0,0 +1,47 @@ +import { isNumber, isPlainObject } from 'support/utils/types'; +import { windowSize, offsetSize, clientSize, getBoundingClientRect } from 'support/dom/dimensions'; + +describe('dom dimensions', () => { + describe('offsetSize', () => { + test('DOM element', () => { + const result = offsetSize(document.body); + expect(isPlainObject(result)).toBe(true); + expect(isNumber(result.w)).toBe(true); + expect(isNumber(result.h)).toBe(true); + }); + + test('null', () => { + const result = offsetSize(null); + expect(isPlainObject(result)).toBe(true); + expect(isNumber(result.w)).toBe(true); + expect(isNumber(result.h)).toBe(true); + }); + }); + + describe('clientSize', () => { + test('DOM element', () => { + const result = clientSize(document.body); + expect(isPlainObject(result)).toBe(true); + expect(isNumber(result.w)).toBe(true); + expect(isNumber(result.h)).toBe(true); + }); + + test('null', () => { + const result = clientSize(null); + expect(isPlainObject(result)).toBe(true); + expect(isNumber(result.w)).toBe(true); + expect(isNumber(result.h)).toBe(true); + }); + }); + + test('windowSize', () => { + const result = windowSize(); + expect(isPlainObject(result)).toBe(true); + expect(isNumber(result.w)).toBe(true); + expect(isNumber(result.h)).toBe(true); + }); + + test('getBoundingClientRect', () => { + expect(getBoundingClientRect(document.body)).toEqual(document.body.getBoundingClientRect()); + }); +}); diff --git a/packages/overlayscrollbars/tests/support/dom/offset.test.ts b/packages/overlayscrollbars/tests/support/dom/offset.test.ts index dec3cc6..78245a4 100644 --- a/packages/overlayscrollbars/tests/support/dom/offset.test.ts +++ b/packages/overlayscrollbars/tests/support/dom/offset.test.ts @@ -3,14 +3,14 @@ import { offset, position } from 'support/dom/offset'; describe('dom offset', () => { describe('offset', () => { - test('returns correct object with DOM element', () => { + test('DOM element', () => { const result = offset(document.body); expect(isPlainObject(result)).toBe(true); expect(isNumber(result.x)).toBe(true); expect(isNumber(result.y)).toBe(true); }); - test('returns correct object with null', () => { + test('null', () => { const result = offset(null); expect(isPlainObject(result)).toBe(true); expect(result.x).toBe(0); @@ -19,14 +19,14 @@ describe('dom offset', () => { }); describe('position', () => { - test('returns correct object with DOM element', () => { + test('DOM element', () => { const result = position(document.body); expect(isPlainObject(result)).toBe(true); expect(isNumber(result.x)).toBe(true); expect(isNumber(result.y)).toBe(true); }); - test('returns correct object with null', () => { + test('null', () => { const result = position(null); expect(isPlainObject(result)).toBe(true); expect(result.x).toBe(0); diff --git a/packages/overlayscrollbars/tests/support/utils/arrays.test.ts b/packages/overlayscrollbars/tests/support/utils/arrays.test.ts index 30c1a25..d3b9bec 100644 --- a/packages/overlayscrollbars/tests/support/utils/arrays.test.ts +++ b/packages/overlayscrollbars/tests/support/utils/arrays.test.ts @@ -1,4 +1,4 @@ -import { each, indexOf } from 'support/utils/array'; +import { each, from, indexOf } from 'support/utils/array'; describe('array utilities', () => { describe('each', () => { @@ -172,6 +172,26 @@ describe('array utilities', () => { }); }); + describe('from', () => { + test('Array.from', () => { + document.body.innerHTML = '
'; + const fromChildNodes = from(document.body.childNodes); + expect(fromChildNodes).toEqual(Array.from(document.body.childNodes)); + document.body.innerHTML = ''; + }); + + test('fallback', () => { + document.body.innerHTML = '
'; + const arrFrom = Array.from; + // @ts-ignore + Array.from = undefined; + const fromChildNodes = from(document.body.childNodes); + Array.from = arrFrom; + expect(fromChildNodes).toEqual(Array.from(document.body.childNodes)); + document.body.innerHTML = ''; + }); + }); + test('indexOf', () => { const idx = indexOf([1, 2, 3], 2); expect(idx).toBe(1); diff --git a/packages/overlayscrollbars/tests/support/utils/extend.test.ts b/packages/overlayscrollbars/tests/support/utils/extend.test.ts deleted file mode 100644 index 4ee6125..0000000 --- a/packages/overlayscrollbars/tests/support/utils/extend.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { extend } from 'support/utils/extend'; -import { isPlainObject } from 'support/utils/types'; - -// type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial } : T -type Deep = { - foo?: { - bar?: boolean; - baz?: boolean; - }; - foo2?: Document; -}; -type Settings = { - xnumber0?: null; - xnumber1?: number | null; - xnumber2?: number | null; - xstring1?: string; - xstring2?: string; - xxx?: string; -}; -type NestedArray = { - arr: Array | object; -}; - -// https://github.com/jquery/jquery/blob/master/test/unit/core.js#L965 -describe('extend', () => { - test('equals object assign', () => { - let settings: Settings = { xnumber1: 5, xnumber2: 7, xstring1: 'peter', xstring2: 'pan' }; - const options: Settings = { xnumber2: 1, xstring2: 'x', xxx: 'newstring' }; - const optionsCopy: Settings = { xnumber2: 1, xstring2: 'x', xxx: 'newstring' }; - const merged: Settings = { xnumber1: 5, xnumber2: 1, xstring1: 'peter', xstring2: 'x', xxx: 'newstring' }; - - extend(settings, options); - expect(settings).toEqual(merged); - expect(options).toEqual(optionsCopy); - - extend(settings, null, options); - expect(settings).toEqual(merged); - expect(options).toEqual(optionsCopy); - - const deep1: Deep = { foo: { bar: true } }; - const deep2: Deep = { foo: { baz: true }, foo2: document }; - const deep2copy: Deep = { foo: { baz: true }, foo2: document }; - const deepmerged: Deep = { foo: { bar: true, baz: true }, foo2: document }; - - extend(deep1, deep2); - expect(deep1.foo).toEqual(deepmerged.foo); - expect(deep2.foo).toEqual(deep2copy.foo); - expect(deep1.foo2).toBe(document); - - const arr = [1, 2, 3]; - const nestedArray: NestedArray = { arr }; - - expect(extend({}, nestedArray).arr).not.toBe(arr); - expect(Array.isArray(extend({ arr: {} }, nestedArray).arr)).toBeTruthy(); - expect(Array.isArray(extend({ arr: {} }, nestedArray).arr)).toBeTruthy(); - expect(isPlainObject(extend({ arr }, { arr: {} }).arr)).toBeTruthy(); - - let empty: { foo?: any } = {}; - const optionsWithLength = { foo: { length: -1 } }; - - extend(empty, optionsWithLength); - expect(empty.foo).toEqual(optionsWithLength.foo); - - empty = {}; - const optionsWithDate = { foo: { date: new Date() } }; - - extend(empty, optionsWithDate); - expect(empty.foo).toEqual(optionsWithDate.foo); - - /** @constructor */ - const MyKlass = function () {}; - // @ts-ignore - const customObject = new MyKlass(); - const optionsWithCustomObject = { foo: { date: customObject } }; - empty = {}; - - extend(empty, optionsWithCustomObject); - expect(empty.foo && empty.foo.date === customObject).toBeTruthy(); - - // Makes the class a little more realistic - MyKlass.prototype = { someMethod() {} }; - empty = {}; - - extend(empty, optionsWithCustomObject); - expect(empty.foo && empty.foo.date === customObject).toBeTruthy(); - - const MyNumber = Number; - - let ret: any = extend({ foo: 4 }, { foo: new MyNumber(5) }); - expect(parseInt(ret.foo?.toString() as string, 10) === 5).toBeTruthy(); - - let nullUndef = extend({}, options, { xnumber2: null }); - expect(nullUndef.xnumber2).toBe(null); - - // @ts-ignore - nullUndef = extend({}, options, { xnumber2: undefined }); - expect(nullUndef.xnumber2).toBe(options.xnumber2); - - // @ts-ignore - nullUndef = extend({}, options, { xnumber0: null }); - expect(nullUndef.xnumber0).toBe(null); - - const target = {}; - const recursive = { foo: target, bar: 5 }; - - extend(target, recursive); - expect(target).toEqual({ bar: 5 }); - - ret = extend({ foo: [] }, { foo: [0] }); - expect(ret.foo?.length).toBe(1); - - ret = extend({ foo: '1,2,3' }, { foo: [1, 2, 3] }); - expect(typeof ret.foo !== 'string').toBeTruthy(); - - ret = extend({ foo: 'bar' }, { foo: null }); - expect(typeof ret.foo !== 'undefined').toBeTruthy(); - - const obj = { foo: null }; - extend(obj, { foo: 'notnull' }); - expect(obj.foo).toBe('notnull'); - - const func: { (): void; key?: string } = () => {}; - extend(func, { key: 'value' }); - expect(func.key).toBe('value'); - - const defaults = { xnumber1: 5, xnumber2: 7, xstring1: 'peter', xstring2: 'pan' }; - const defaultsCopy = { xnumber1: 5, xnumber2: 7, xstring1: 'peter', xstring2: 'pan' }; - const options1 = { xnumber2: 1, xstring2: 'x' }; - const options1Copy = { xnumber2: 1, xstring2: 'x' }; - const options2 = { xstring2: 'xx', xxx: 'newstringx' }; - const options2Copy = { xstring2: 'xx', xxx: 'newstringx' }; - const merged2 = { xnumber1: 5, xnumber2: 1, xstring1: 'peter', xstring2: 'xx', xxx: 'newstringx' }; - - settings = extend({}, defaults, options1, options2); - expect(settings).toEqual(merged2); - expect(defaults).toEqual(defaultsCopy); - expect(options1).toEqual(options1Copy); - expect(options2).toEqual(options2Copy); - - expect(extend('', { foo: 1 })).toEqual({ foo: 1 }); - expect(extend(null, { foo: null, deep: { foo: null } })).toEqual({ foo: null, deep: { foo: null } }); - expect(extend(12, { foo: 1, deep: { foo: null, text: '' } })).toEqual({ foo: 1, deep: { foo: null, text: '' } }); - }); -}); diff --git a/packages/overlayscrollbars/tests/support/utils/object.test.ts b/packages/overlayscrollbars/tests/support/utils/object.test.ts index a84d71c..e771c11 100644 --- a/packages/overlayscrollbars/tests/support/utils/object.test.ts +++ b/packages/overlayscrollbars/tests/support/utils/object.test.ts @@ -1,6 +1,149 @@ -import { keys, hasOwnProperty } from 'support/utils/object'; +import { extend, keys, hasOwnProperty } from 'support/utils/object'; +import { isPlainObject } from 'support/utils/types'; describe('object utilities', () => { + // https://github.com/jquery/jquery/blob/master/test/unit/core.js#L965 + describe('extend', () => { + // type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial } : T + type Deep = { + foo?: { + bar?: boolean; + baz?: boolean; + }; + foo2?: Document; + }; + type Settings = { + xnumber0?: null; + xnumber1?: number | null; + xnumber2?: number | null; + xstring1?: string; + xstring2?: string; + xxx?: string; + }; + type NestedArray = { + arr: Array | object; + }; + + test('equals object assign', () => { + let settings: Settings = { xnumber1: 5, xnumber2: 7, xstring1: 'peter', xstring2: 'pan' }; + const options: Settings = { xnumber2: 1, xstring2: 'x', xxx: 'newstring' }; + const optionsCopy: Settings = { xnumber2: 1, xstring2: 'x', xxx: 'newstring' }; + const merged: Settings = { xnumber1: 5, xnumber2: 1, xstring1: 'peter', xstring2: 'x', xxx: 'newstring' }; + + extend(settings, options); + expect(settings).toEqual(merged); + expect(options).toEqual(optionsCopy); + + extend(settings, null, options); + expect(settings).toEqual(merged); + expect(options).toEqual(optionsCopy); + + const deep1: Deep = { foo: { bar: true } }; + const deep2: Deep = { foo: { baz: true }, foo2: document }; + const deep2copy: Deep = { foo: { baz: true }, foo2: document }; + const deepmerged: Deep = { foo: { bar: true, baz: true }, foo2: document }; + + extend(deep1, deep2); + expect(deep1.foo).toEqual(deepmerged.foo); + expect(deep2.foo).toEqual(deep2copy.foo); + expect(deep1.foo2).toBe(document); + + const arr = [1, 2, 3]; + const nestedArray: NestedArray = { arr }; + + expect(extend({}, nestedArray).arr).not.toBe(arr); + expect(Array.isArray(extend({ arr: {} }, nestedArray).arr)).toBeTruthy(); + expect(Array.isArray(extend({ arr: {} }, nestedArray).arr)).toBeTruthy(); + expect(isPlainObject(extend({ arr }, { arr: {} }).arr)).toBeTruthy(); + + let empty: { foo?: any } = {}; + const optionsWithLength = { foo: { length: -1 } }; + + extend(empty, optionsWithLength); + expect(empty.foo).toEqual(optionsWithLength.foo); + + empty = {}; + const optionsWithDate = { foo: { date: new Date() } }; + + extend(empty, optionsWithDate); + expect(empty.foo).toEqual(optionsWithDate.foo); + + /** @constructor */ + const MyKlass = function () {}; + // @ts-ignore + const customObject = new MyKlass(); + const optionsWithCustomObject = { foo: { date: customObject } }; + empty = {}; + + extend(empty, optionsWithCustomObject); + expect(empty.foo && empty.foo.date === customObject).toBeTruthy(); + + // Makes the class a little more realistic + MyKlass.prototype = { someMethod() {} }; + empty = {}; + + extend(empty, optionsWithCustomObject); + expect(empty.foo && empty.foo.date === customObject).toBeTruthy(); + + const MyNumber = Number; + + let ret: any = extend({ foo: 4 }, { foo: new MyNumber(5) }); + expect(parseInt(ret.foo?.toString() as string, 10) === 5).toBeTruthy(); + + let nullUndef = extend({}, options, { xnumber2: null }); + expect(nullUndef.xnumber2).toBe(null); + + // @ts-ignore + nullUndef = extend({}, options, { xnumber2: undefined }); + expect(nullUndef.xnumber2).toBe(options.xnumber2); + + // @ts-ignore + nullUndef = extend({}, options, { xnumber0: null }); + expect(nullUndef.xnumber0).toBe(null); + + const target = {}; + const recursive = { foo: target, bar: 5 }; + + extend(target, recursive); + expect(target).toEqual({ bar: 5 }); + + ret = extend({ foo: [] }, { foo: [0] }); + expect(ret.foo?.length).toBe(1); + + ret = extend({ foo: '1,2,3' }, { foo: [1, 2, 3] }); + expect(typeof ret.foo !== 'string').toBeTruthy(); + + ret = extend({ foo: 'bar' }, { foo: null }); + expect(typeof ret.foo !== 'undefined').toBeTruthy(); + + const obj = { foo: null }; + extend(obj, { foo: 'notnull' }); + expect(obj.foo).toBe('notnull'); + + const func: { (): void; key?: string } = () => {}; + extend(func, { key: 'value' }); + expect(func.key).toBe('value'); + + const defaults = { xnumber1: 5, xnumber2: 7, xstring1: 'peter', xstring2: 'pan' }; + const defaultsCopy = { xnumber1: 5, xnumber2: 7, xstring1: 'peter', xstring2: 'pan' }; + const options1 = { xnumber2: 1, xstring2: 'x' }; + const options1Copy = { xnumber2: 1, xstring2: 'x' }; + const options2 = { xstring2: 'xx', xxx: 'newstringx' }; + const options2Copy = { xstring2: 'xx', xxx: 'newstringx' }; + const merged2 = { xnumber1: 5, xnumber2: 1, xstring1: 'peter', xstring2: 'xx', xxx: 'newstringx' }; + + settings = extend({}, defaults, options1, options2); + expect(settings).toEqual(merged2); + expect(defaults).toEqual(defaultsCopy); + expect(options1).toEqual(options1Copy); + expect(options2).toEqual(options2Copy); + + expect(extend('', { foo: 1 })).toEqual({ foo: 1 }); + expect(extend(null, { foo: null, deep: { foo: null } })).toEqual({ foo: null, deep: { foo: null } }); + expect(extend(12, { foo: 1, deep: { foo: null, text: '' } })).toEqual({ foo: 1, deep: { foo: null, text: '' } }); + }); + }); + describe('keys', () => { test('correct amount of keys', () => { const obj = {