improve structure

This commit is contained in:
Rene Haas
2020-09-01 13:28:37 +02:00
parent dcaebba030
commit 6675b0d6db
9 changed files with 282 additions and 218 deletions
@@ -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;
@@ -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<T, U>(target: T, object1: U): T & U;
export function extend<T, U, V>(target: T, object1: U, object2: V): T & U & V;
export function extend<T, U, V, W>(target: T, object1: U, object2: V, object3: W): T & U & V & W;
export function extend<T, U, V, W, X>(target: T, object1: U, object2: V, object3: W, object4: X): T & U & V & W & X;
export function extend<T, U, V, W, X, Y>(target: T, object1: U, object2: V, object3: W, object4: X, object5: Y): T & U & V & W & X & Y;
export function extend<T, U, V, W, X, Y, Z>(
target: T,
object1?: U,
object2?: V,
object3?: W,
object4?: X,
object5?: Y,
object6?: Z
): T & U & V & W & X & Y & Z {
const sources: Array<any> = [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;
}
@@ -1,4 +1,3 @@
export * from 'support/utils/array';
export * from 'support/utils/object';
export * from 'support/utils/extend';
export * from 'support/utils/types';
@@ -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<string> => (obj ? Object.keys(obj) : []);
// https://github.com/jquery/jquery/blob/master/src/core.js#L116
export function extend<T, U>(target: T, object1: U): T & U;
export function extend<T, U, V>(target: T, object1: U, object2: V): T & U & V;
export function extend<T, U, V, W>(target: T, object1: U, object2: V, object3: W): T & U & V & W;
export function extend<T, U, V, W, X>(target: T, object1: U, object2: V, object3: W, object4: X): T & U & V & W & X;
export function extend<T, U, V, W, X, Y>(target: T, object1: U, object2: V, object3: W, object4: X, object5: Y): T & U & V & W & X & Y;
export function extend<T, U, V, W, X, Y, Z>(
target: T,
object1?: U,
object2?: V,
object3?: W,
object4?: X,
object5?: Y,
object6?: Z
): T & U & V & W & X & Y & Z {
const sources: Array<any> = [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;
}
@@ -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());
});
});
@@ -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);
@@ -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 = '<div></div><div></div><div></div>';
const fromChildNodes = from(document.body.childNodes);
expect(fromChildNodes).toEqual(Array.from(document.body.childNodes));
document.body.innerHTML = '';
});
test('fallback', () => {
document.body.innerHTML = '<div></div><div></div><div></div>';
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);
@@ -1,144 +0,0 @@
import { extend } from 'support/utils/extend';
import { isPlainObject } from 'support/utils/types';
// type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : 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<any> | 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: '' } });
});
});
@@ -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> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : 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<any> | 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 = {