add cache support and start structure lifecycle

This commit is contained in:
Rene
2020-12-06 02:16:14 +01:00
parent cdc0aecd2f
commit 6facbcb687
24 changed files with 815 additions and 53 deletions
@@ -12,6 +12,7 @@ import {
removeElements,
windowSize,
runEach,
equalWH,
} from 'support';
export type OnEnvironmentChanged = (env: Environment) => void;
@@ -21,6 +22,7 @@ export interface Environment {
_nativeScrollbarIsOverlaid: XY<boolean>;
_nativeScrollbarStyling: boolean;
_rtlScrollBehavior: { n: boolean; i: boolean };
_flexboxGlue: boolean;
_addListener(listener: OnEnvironmentChanged): void;
_removeListener(listener: OnEnvironmentChanged): void;
}
@@ -28,6 +30,8 @@ export interface Environment {
let environmentInstance: Environment;
const { abs, round } = Math;
const environmentElmId = 'os-environment';
const classNameFlexboxGlue = 'flexbox-glue';
const classNameFlexboxGlueMax = `${classNameFlexboxGlue}-max`;
const getNativeScrollbarSize = (body: HTMLElement, measureElm: HTMLElement): XY => {
appendChildren(body, measureElm);
@@ -42,7 +46,7 @@ const getNativeScrollbarSize = (body: HTMLElement, measureElm: HTMLElement): XY
const getNativeScrollbarStyling = (testElm: HTMLElement): boolean => {
let result = false;
addClass(testElm, 'os-viewport-native-scrollbars-invisible');
addClass(testElm, 'os-viewport-scrollbar-styled');
try {
result =
style(testElm, 'scrollbar-width') === 'none' || window.getComputedStyle(testElm, '::-webkit-scrollbar').getPropertyValue('display') === 'none';
@@ -78,6 +82,20 @@ const getRtlScrollBehavior = (parentElm: HTMLElement, childElm: HTMLElement): {
};
};
const getFlexboxGlue = (parentElm: HTMLElement, childElm: HTMLElement): boolean => {
addClass(parentElm, classNameFlexboxGlue);
const minOffsetsizeParent = offsetSize(parentElm);
const minOffsetsize = offsetSize(childElm);
const supportsMin = equalWH(minOffsetsize, minOffsetsizeParent);
addClass(parentElm, classNameFlexboxGlueMax);
const maxOffsetsizeParent = offsetSize(parentElm);
const maxOffsetsize = offsetSize(childElm);
const supportsMax = equalWH(maxOffsetsize, maxOffsetsizeParent);
return supportsMin && supportsMax;
};
const getWindowDPR = (): number => {
// eslint-disable-next-line
// @ts-ignore
@@ -113,6 +131,7 @@ const createEnvironment = (): Environment => {
_nativeScrollbarIsOverlaid: nativeScrollbarIsOverlaid,
_nativeScrollbarStyling: getNativeScrollbarStyling(envElm),
_rtlScrollBehavior: getRtlScrollBehavior(envElm, envChildElm),
_flexboxGlue: getFlexboxGlue(envElm, envChildElm),
_addListener(listener: OnEnvironmentChanged): void {
onChangedListener.add(listener);
},
@@ -122,6 +141,7 @@ const createEnvironment = (): Environment => {
};
removeAttr(envElm, 'style');
removeAttr(envElm, 'class');
removeElements(envElm);
if (!nativeScrollbarIsOverlaid.x || !nativeScrollbarIsOverlaid.y) {
@@ -1,19 +1,57 @@
@import './sizeobserver.scss';
@import './trinsicobserver.scss';
@import './structurelifecycle.scss';
#os-environment {
position: fixed;
opacity: 0;
visibility: hidden;
overflow: scroll;
height: 500px;
width: 500px;
}
#os-environment > div {
width: 200%;
height: 200%;
margin: 10px 0;
height: 200px;
width: 200px;
div {
width: 200%;
height: 200%;
margin: 10px 0;
}
&.flexbox-glue {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
height: auto;
width: auto;
min-height: 200px;
min-width: 200px;
div {
flex: auto;
width: auto;
height: auto;
max-height: 100%;
max-width: 100%;
margin: 0;
}
}
&.flexbox-glue-max {
max-height: 200px;
div {
overflow: visible;
&::before {
content: '';
display: block;
height: 999px;
width: 999px;
}
}
}
}
#os-environment
/* fix restricted measuring */
#os-environment:before,
#os-environment:after,
@@ -33,14 +71,14 @@
.os-viewport {
-ms-overflow-style: scrollbar !important;
}
.os-viewport-native-scrollbars-invisible#os-environment,
.os-viewport-native-scrollbars-invisible.os-viewport {
.os-viewport-scrollbar-styled#os-environment,
.os-viewport-scrollbar-styled.os-viewport {
scrollbar-width: none !important;
}
.os-viewport-native-scrollbars-invisible#os-environment::-webkit-scrollbar,
.os-viewport-native-scrollbars-invisible.os-viewport::-webkit-scrollbar,
.os-viewport-native-scrollbars-invisible#os-environment::-webkit-scrollbar-corner,
.os-viewport-native-scrollbars-invisible.os-viewport::-webkit-scrollbar-corner {
.os-viewport-scrollbar-styled#os-environment::-webkit-scrollbar,
.os-viewport-scrollbar-styled.os-viewport::-webkit-scrollbar,
.os-viewport-scrollbar-styled#os-environment::-webkit-scrollbar-corner,
.os-viewport-scrollbar-styled.os-viewport::-webkit-scrollbar-corner {
display: none !important;
width: 0px !important;
height: 0px !important;
@@ -1,18 +1,81 @@
import { OverlayScrollbarsLifecycle } from 'overlayscrollbars/lifecycles';
import {
cssProperty,
createDOM,
runEach,
contents,
appendChildren,
removeElements,
addClass,
topRightBottomLeft,
TRBL,
equalTRBL,
createCache,
} from 'support';
import { Lifecycle } from 'overlayscrollbars/lifecycles';
import { getEnvironment, Environment } from 'environment';
import { createSizeObserver } from 'overlayscrollbars/observers/SizeObserver';
import { createTrinsicObserver } from 'overlayscrollbars/observers/TrinsicObserver';
export type OverflowBehavior = 'hidden' | 'scroll' | 'visible-hidden' | 'visible-scroll';
export interface StructureLifecycleOptions {
_paddingAbsolute: boolean;
_autoSizeCapable: boolean;
_heightAuto: boolean;
_widthAuto: boolean;
_border: [number, number, number, number];
_padding: [number, number, number, number];
_margin: [number, number, number, number];
_paddingAbsolute?: boolean;
_overflowBehavior?: {
x: OverflowBehavior;
y: OverflowBehavior;
};
}
export class StructureLifecycle extends OverlayScrollbarsLifecycle<StructureLifecycleOptions> {
// eslint-disable-next-line
_update(options?: StructureLifecycleOptions): void {}
// eslint-disable-next-line
_destruct(): void {}
interface StructureLifecycleCache {
padding: TRBL;
}
const classNameHost = 'os-host';
const classNameViewport = 'os-viewport';
const classNameContent = 'os-content';
const classNameViewportScrollbarStyling = `${classNameViewport}-scrollbar-styled`;
const cssMarginEnd = cssProperty('margin-inline-end');
const cssBorderEnd = cssProperty('border-inline-end');
export const createStructureLifecycle = (target: HTMLElement, options?: StructureLifecycleOptions): Lifecycle<StructureLifecycleOptions> => {
const destructFns: (() => any)[] = [];
const env: Environment = getEnvironment();
const scrollbarsOverlaid = env._nativeScrollbarIsOverlaid;
const supportsScrollbarStyling = env._nativeScrollbarStyling;
const supportFlexboxGlue = env._flexboxGlue;
// direction change is only needed to update scrollbar hiding, therefore its not needed if css can do it, scrollbars are invisible or overlaid on y axis
const directionObserverObsolete = (cssMarginEnd && cssBorderEnd) || supportsScrollbarStyling || scrollbarsOverlaid.y;
const viewportElm = createDOM(`<div class="${classNameViewport} ${classNameViewportScrollbarStyling}"></div>`)[0];
const contentElm = createDOM(`<div class="${classNameContent}"></div>`)[0];
const updateCache = createCache<StructureLifecycleCache>({
padding: [() => topRightBottomLeft(target, 'padding'), equalTRBL],
});
const onSizeChanged = (direction?: 'ltr' | 'rtl') => {
updateCache('padding');
};
const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsic: boolean) => {
console.log('heightAuot', heightIntrinsic);
};
appendChildren(viewportElm, contentElm);
appendChildren(contentElm, contents(target));
appendChildren(target, viewportElm);
addClass(target, classNameHost);
destructFns.push(createSizeObserver(target, onSizeChanged, { _appear: true, _direction: !directionObserverObsolete }));
destructFns.push(createTrinsicObserver(target, onTrinsicChanged));
return {
_options() {
// eslint-disable-next-line
console.log('_options');
},
_destruct() {
runEach(destructFns);
removeElements(viewportElm);
},
};
};
@@ -1,14 +1,6 @@
import { PlainObject } from 'typings';
import { Environment } from 'environment';
export abstract class OverlayScrollbarsLifecycle<T extends PlainObject> {
protected environment: Environment;
constructor(environment: Environment) {
this.environment = environment;
}
abstract _update(options?: T): void;
abstract _destruct(): void;
export interface Lifecycle<T = PlainObject> {
_options(options?: T): void;
_destruct(): void;
}
@@ -13,6 +13,8 @@ import {
preventDefault,
stopPropagation,
addClass,
isString,
equalWH,
} from 'support';
import { getEnvironment } from 'environment';
@@ -31,11 +33,11 @@ const getDirection = (elm: HTMLElement) => style(elm, 'direction');
// TODO:
// 1. MAYBE add comparison function to offsetSize etc.
type Direction = 'ltr' | 'rtl';
export type SizeObserverOptions = { _direction?: boolean; _appear?: boolean };
export const createSizeObserver = (
target: HTMLElement,
onSizeChangedCallback: (direction?: boolean) => any,
onSizeChangedCallback: (direction?: Direction) => any,
options?: SizeObserverOptions
): (() => void) => {
const { _direction: direction = false, _appear: appear = false } = options || {};
@@ -43,13 +45,13 @@ export const createSizeObserver = (
const baseElements = createDOM(`<div class="${classNameSizeObserver}"><div class="${classNameSizeObserverListener}"></div></div>`);
const sizeObserver = baseElements[0] as HTMLElement;
const listenerElement = sizeObserver.firstChild as HTMLElement;
const onSizeChangedCallbackProxy = (dir?: boolean) => {
const onSizeChangedCallbackProxy = (dir?: Direction) => {
if (direction) {
const rtl = getDirection(sizeObserver) === 'rtl';
scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount);
scrollTop(sizeObserver, scrollAmount);
}
onSizeChangedCallback(dir === true);
onSizeChangedCallback(isString(dir) ? dir : undefined);
};
const offListeners: (() => void)[] = [];
let appearCallback: ((...args: any) => any) | null = appear ? onSizeChangedCallbackProxy : null;
@@ -90,7 +92,7 @@ export const createSizeObserver = (
};
const onScroll = (scrollEvent?: Event) => {
currSize = offsetSize(listenerElement);
isDirty = !scrollEvent || currSize.w !== cacheSize.w || currSize.h !== cacheSize.h;
isDirty = !scrollEvent || !equalWH(currSize, cacheSize);
if (scrollEvent && isDirty && !rAFId) {
cAF(rAFId);
@@ -132,7 +134,7 @@ export const createSizeObserver = (
style(listenerElement, { left: 0, right: 'auto' });
}
dirCache = dir;
onSizeChangedCallbackProxy(true);
onSizeChangedCallbackProxy(dir as Direction);
}
preventDefault(event);
@@ -38,7 +38,7 @@ export const createTrinsicObserver = (
const newHeightIntrinsic = newSize.h === 0;
if (newHeightIntrinsic !== heightIntrinsic) {
onTrinsicChangedCallback(false, newSize.h === 0);
onTrinsicChangedCallback(false, newHeightIntrinsic);
heightIntrinsic = newHeightIntrinsic;
}
})
@@ -0,0 +1,23 @@
.os-host {
position: relative;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.os-viewport {
box-sizing: border-box;
position: relative;
flex: auto;
height: auto;
width: auto;
padding: 0;
margin: 0;
border: none;
overflow: visible;
}
.os-content {
position: relative;
z-index: 0;
}
+87
View File
@@ -0,0 +1,87 @@
import { each, keys, isArray, isString } from 'support';
interface CacheEntry<T> {
_current?: T;
_previous?: T;
_changed?: boolean;
}
type Cache<T> = {
[P in keyof T]: CacheEntry<T[P]>;
};
type UpdateCacheProp<T> = <P extends keyof T>(prop: P, value: T[P], compare: (a?: T[P], b?: T[P]) => boolean) => void;
export type CacheUpdateFunction<T, P extends keyof T> = (current?: T[P], previous?: T[P]) => T[P];
export type CacheEqualFunction<T, P extends keyof T> = (a?: T[P], b?: T[P]) => boolean;
export type CacheUpdate<T> = {
[P in keyof T]: boolean;
};
export type CacheUpdateInfo<T> = {
[P in keyof T]: CacheUpdateFunction<T, P> | [CacheUpdateFunction<T, P>, CacheEqualFunction<T, P>];
};
/**
* Creates a internally managed generic cache which can be updated by the returned function.
* @param cacheUpdateInfo A object which accepts a function or a tuple of functions as values for its properties.
* {
* name: updateFn,
* // or
* name: [updateFn, equalFn]
* }
* The first function is the update function (updateFn) which is executed when this cache prop shall be updated.
* Two params are passed, the first one is the current cache value and the second one is the previous cache value.
*
* The second function is the equal function (equalFn) which is also executed when this cache prop shall be updated,
* but returns a boolean which indicates whether the current value and the new updated value are equal.
* If no equal function is passed a shallow comparison is carried out between the values.
*
* @returns A function which can be called with wither one ar an array of properties which shall be updated. Optionally it can be called with the force param.
* This function returns a object which contains all cache properties as booleans which indicate whether the corresponding cache values really changed or not.
*/
export const createCache = <T>(
cacheUpdateInfo: CacheUpdateInfo<T>
): ((propsToUpdate?: Array<keyof T> | keyof T, force?: boolean) => CacheUpdate<T>) => {
const cache: Cache<T> = {} as T;
const allProps: Array<keyof T> = keys(cacheUpdateInfo) as Array<keyof T>;
each(allProps, (prop) => {
cache[prop] = {};
});
const updateCacheProp: UpdateCacheProp<T> = (prop, value, equal): void => {
const curr = cache[prop]._current;
cache[prop]._current = value;
cache[prop]._previous = curr;
cache[prop]._changed = equal ? !equal(curr, value) : curr !== value;
};
const flush = (force?: boolean): CacheUpdate<T> => {
const result: CacheUpdate<T> = {} as CacheUpdate<T>;
each(allProps, (prop: keyof T) => {
result[prop] = !!(cache[prop]._changed || force);
cache[prop]._changed = false;
});
return result;
};
return (propsToUpdate?: Array<keyof T> | keyof T, force?: boolean) => {
const finalPropsToUpdate: Array<keyof T> =
(isString(propsToUpdate) ? ([propsToUpdate] as Array<keyof T>) : (propsToUpdate as Array<keyof T>)) || allProps;
each(finalPropsToUpdate, (prop) => {
const cacheVal = cache[prop];
const curr = cacheUpdateInfo[prop];
const arr = isArray(curr);
const value = arr ? curr[0] : curr;
const equal = arr ? curr[1] : null;
updateCacheProp(prop, value(cacheVal._current, cacheVal._previous), equal);
});
return flush(force);
};
};
+1
View File
@@ -0,0 +1 @@
export * from 'support/cache/cache';
@@ -2,6 +2,13 @@ import { each, keys } from 'support/utils';
import { isString, isNumber, isArray } from 'support/utils/types';
import { PlainObject } from 'typings';
export interface TRBL {
t: number;
r: number;
b: number;
l: number;
}
type CssStyles = { [key: string]: string | number };
const cssNumber = {
animationiterationcount: 1,
@@ -19,6 +26,12 @@ const cssNumber = {
zoom: 1,
};
const parseToZeroOrNumber = (value: string, toFloat?: boolean): number => {
/* istanbul ignore next */
const num = toFloat ? parseFloat(value) : parseInt(value, 10);
/* istanbul ignore next */
return Number.isNaN(num) ? 0 : num;
};
const adaptCSSVal = (prop: string, val: string | number): string | number => (!cssNumber[prop.toLowerCase()] && isNumber(val) ? `${val}px` : val);
const getCSSVal = (elm: HTMLElement, computedStyle: CSSStyleDeclaration, prop: string): string =>
/* istanbul ignore next */
@@ -74,3 +87,23 @@ export const hide = (elm: HTMLElement | null): void => {
export const show = (elm: HTMLElement | null): void => {
style(elm, { display: 'block' });
};
/**
* Returns a top
* @param elm
* @param property
*/
export const topRightBottomLeft = (elm: HTMLElement | null, property?: string): TRBL => {
const finalProp = property || '';
const top = `${finalProp}Top`;
const right = `${finalProp}Right`;
const bottom = `${finalProp}Bottom`;
const left = `${finalProp}Left`;
const result = style(elm, [top, right, bottom, left]);
return {
t: parseToZeroOrNumber(result[top]),
r: parseToZeroOrNumber(result[right]),
b: parseToZeroOrNumber(result[bottom]),
l: parseToZeroOrNumber(result[left]),
};
};
@@ -1,3 +1,4 @@
export * from 'support/cache';
export * from 'support/compatibility';
export * from 'support/dom';
export * from 'support/options';
@@ -0,0 +1,46 @@
import { each } from 'support/utils/array';
import { WH, XY, TRBL } from 'support/dom';
import { PlainObject } from 'typings';
/**
* Compares two objects and returns true if all values of the passed prop names are identical, false otherwise or if one of the two object is falsy.
* @param a Object a.
* @param b Object b.
* @param props The props which shall be compared.
*/
export const equal = <T extends PlainObject>(a: T | undefined, b: T | undefined, props: Array<keyof T>): boolean => {
if (a && b) {
let result = true;
each(props, (prop) => {
if (a[prop] !== b[prop]) {
result = false;
}
});
return result;
}
return false;
};
/**
* Compares object a with object b and returns true if both have the same property values, false otherwise.
* Also returns false if one of the objects is undefined or null.
* @param a Object a.
* @param b Object b.
*/
export const equalWH = (a?: WH, b?: WH) => equal<WH>(a, b, ['w', 'h']);
/**
* Compares object a with object b and returns true if both have the same property values, false otherwise.
* Also returns false if one of the objects is undefined or null.
* @param a Object a.
* @param b Object b.
*/
export const equalXY = (a?: XY, b?: XY) => equal<XY>(a, b, ['x', 'y']);
/**
* Compares object a with object b and returns true if both have the same property values, false otherwise.
* Also returns false if one of the objects is undefined or null.
* @param a Object a.
* @param b Object b.
*/
export const equalTRBL = (a?: TRBL, b?: TRBL) => equal<TRBL>(a, b, ['t', 'r', 'b', 'l']);
@@ -1,3 +1,4 @@
export * from 'support/utils/array';
export * from 'support/utils/equal';
export * from 'support/utils/object';
export * from 'support/utils/types';
@@ -14,7 +14,7 @@
&:not(:empty) {
height: calc(100% + 1px);
top: auto;
top: -1px;
& > .os-size-observer {
width: 1000%;
@@ -0,0 +1,191 @@
import { createCache } from 'support/cache';
const createUpdater = <T>(updaterReturn: (i: number) => T) => {
const fn = jest.fn();
let index = 0;
const update = (curr?: T, prev?: T): T => {
fn(curr, prev);
index += 1;
return updaterReturn(index);
};
return [fn, update];
};
describe('cache', () => {
describe('createCache', () => {
test('creates and updates simple cache', () => {
const [updateNumberFn, updateNumber] = createUpdater<number>((i) => i);
const [updateBooleanFn, updateBoolean] = createUpdater<boolean>((i) => !!(i % 2));
const [updateStringFn, updateString] = createUpdater<string>((i) => `${i}`);
const [updateObjFn, updateObj] = createUpdater<object>((i) => ({ [i]: i }));
const updateCache = createCache({
number: updateNumber,
boolean: updateBoolean,
string: updateString,
object: updateObj,
});
expect(updateCache('number').number).toBe(true);
expect(updateNumberFn).toHaveBeenCalledTimes(1);
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('number').number).toBe(true);
expect(updateNumberFn).toHaveBeenCalledTimes(2);
expect(updateNumberFn).toHaveBeenCalledWith(1, undefined);
expect(updateCache('number').number).toBe(true);
expect(updateNumberFn).toHaveBeenCalledTimes(3);
expect(updateNumberFn).toHaveBeenCalledWith(2, 1);
let { string, boolean, object, number } = updateCache('number');
expect(string).toBe(false);
expect(boolean).toBe(false);
expect(object).toBe(false);
expect(number).toBe(true);
expect(updateBooleanFn).not.toHaveBeenCalled();
expect(updateStringFn).not.toHaveBeenCalled();
expect(updateObjFn).not.toHaveBeenCalled();
({ string, boolean, object, number } = updateCache(['string', 'boolean', 'object']));
expect(string).toBe(true);
expect(boolean).toBe(true);
expect(object).toBe(true);
expect(number).toBe(false);
expect(updateBooleanFn).toHaveBeenCalledTimes(1);
expect(updateBooleanFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateStringFn).toHaveBeenCalledTimes(1);
expect(updateStringFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateObjFn).toHaveBeenCalledTimes(1);
expect(updateObjFn).toHaveBeenCalledWith(undefined, undefined);
updateCache(['string', 'boolean', 'object']);
expect(updateBooleanFn).toHaveBeenCalledTimes(2);
expect(updateBooleanFn).toHaveBeenCalledWith(!!(1 % 2), undefined);
expect(updateStringFn).toHaveBeenCalledTimes(2);
expect(updateStringFn).toHaveBeenCalledWith('1', undefined);
expect(updateObjFn).toHaveBeenCalledTimes(2);
expect(updateObjFn).toHaveBeenCalledWith({ 1: 1 }, undefined);
updateCache(['string', 'boolean', 'object']);
expect(updateBooleanFn).toHaveBeenCalledTimes(3);
expect(updateBooleanFn).toHaveBeenCalledWith(!!(2 % 2), !!(1 % 2));
expect(updateStringFn).toHaveBeenCalledTimes(3);
expect(updateStringFn).toHaveBeenCalledWith('2', '1');
expect(updateObjFn).toHaveBeenCalledTimes(3);
expect(updateObjFn).toHaveBeenCalledWith({ 2: 2 }, { 1: 1 });
updateCache(['string', 'boolean', 'object']);
({ string, boolean, object, number } = updateCache());
expect(string).toBe(true);
expect(boolean).toBe(true);
expect(object).toBe(true);
expect(number).toBe(true);
expect(updateBooleanFn).toHaveBeenCalledTimes(5);
expect(updateStringFn).toHaveBeenCalledTimes(5);
expect(updateObjFn).toHaveBeenCalledTimes(5);
expect(updateNumberFn).toHaveBeenCalledTimes(5);
});
test('doesnt update if nothing changes with primitives', () => {
const [updateNumberFn, updateNumber] = createUpdater<number>(() => 0);
const updateCache = createCache({
number: updateNumber,
});
expect(updateCache('number').number).toBe(true);
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('number').number).toBe(false);
expect(updateNumberFn).toHaveBeenCalledWith(0, undefined);
expect(updateCache('number').number).toBe(false);
expect(updateNumberFn).toHaveBeenCalledWith(0, 0);
});
test('doesnt update if nothing changes with non primitives', () => {
const constObj = { a: 0, b: 0 };
const [updateConstObjFn, updateConstObj] = createUpdater<{ a: number; b: number }>(() => constObj);
const [updateSimilarObjFn, updateSimilarObj] = createUpdater<{ a: number; b: number }>(() => ({ ...constObj }));
const [updateComparisonObjFn, updateComparisonObj] = createUpdater<{ a: number; b: number }>(() => ({ ...constObj }));
const updateCache = createCache({
constObj: updateConstObj,
similarObj: updateSimilarObj,
comparisonObj: [
updateComparisonObj,
(a?: { a: number; b: number }, b?: { a: number; b: number }): boolean => !!(a && b && a.a === b.a && a.b === b.b),
],
});
expect(updateCache('constObj').constObj).toBe(true);
expect(updateConstObjFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('constObj').constObj).toBe(false);
expect(updateConstObjFn).toHaveBeenCalledWith(constObj, undefined);
expect(updateCache('constObj').constObj).toBe(false);
expect(updateConstObjFn).toHaveBeenCalledWith(constObj, constObj);
expect(updateCache('similarObj').similarObj).toBe(true);
expect(updateSimilarObjFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('similarObj').similarObj).toBe(true);
expect(updateSimilarObjFn).toHaveBeenCalledWith(constObj, undefined);
expect(updateCache('similarObj').similarObj).toBe(true);
expect(updateSimilarObjFn).toHaveBeenCalledWith(constObj, constObj);
expect(updateCache('comparisonObj').comparisonObj).toBe(true);
expect(updateComparisonObjFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('comparisonObj').comparisonObj).toBe(false);
expect(updateComparisonObjFn).toHaveBeenCalledWith(constObj, undefined);
expect(updateCache('comparisonObj').comparisonObj).toBe(false);
expect(updateComparisonObjFn).toHaveBeenCalledWith(constObj, constObj);
});
test('updates definitely with force', () => {
const [updateNumberFn, updateNumber] = createUpdater<number>(() => 0);
const updateCache = createCache({
number: updateNumber,
});
expect(updateCache('number', true).number).toBe(true);
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('number', true).number).toBe(true);
expect(updateNumberFn).toHaveBeenCalledWith(0, undefined);
expect(updateCache('number', true).number).toBe(true);
expect(updateNumberFn).toHaveBeenCalledWith(0, 0);
});
test('custom comparison on primitves', () => {
const [updateStringFn, updateString] = createUpdater<string>(() => 'hi');
const [updateNumberFn, updateNumber] = createUpdater<number>((i) => i);
const updateCache = createCache({
string: [updateString, () => false],
number: [updateNumber, () => true],
});
expect(updateCache('string').string).toBe(true);
expect(updateStringFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('string').string).toBe(true);
expect(updateStringFn).toHaveBeenCalledWith('hi', undefined);
expect(updateCache('string').string).toBe(true);
expect(updateStringFn).toHaveBeenCalledWith('hi', 'hi');
expect(updateCache('number').number).toBe(false);
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateCache('number').number).toBe(false);
expect(updateNumberFn).toHaveBeenCalledWith(1, undefined);
expect(updateCache('number').number).toBe(false);
expect(updateNumberFn).toHaveBeenCalledWith(2, 1);
});
});
});
@@ -1,5 +1,5 @@
import { isString, isPlainObject, isEmptyObject } from 'support/utils/types';
import { style, hide, show } from 'support/dom/style';
import { style, hide, show, topRightBottomLeft } from 'support/dom/style';
describe('dom style', () => {
afterEach(() => {
@@ -90,4 +90,22 @@ describe('dom style', () => {
expect(show(null)).toBe(undefined);
});
});
describe('topRightBottomLeft', () => {
test('normal', () => {
const result = topRightBottomLeft(document.body);
expect(result.t).toBe(0);
expect(result.r).toBe(0);
expect(result.b).toBe(0);
expect(result.l).toBe(0);
});
test('null', () => {
const result = topRightBottomLeft(null);
expect(result.t).toBe(0);
expect(result.r).toBe(0);
expect(result.b).toBe(0);
expect(result.l).toBe(0);
});
});
});
@@ -0,0 +1,36 @@
import { equal, equalTRBL, equalWH, equalXY } from 'support/utils/equal';
describe('equal', () => {
test('equal', () => {
interface Test {
a: number;
b: number;
c?: number;
}
const equalTest = (a?: Test, b?: Test) => equal<Test>(a, b, ['a', 'b']);
expect(equalTest({ a: 1, b: 1 }, { a: 1, b: 1 })).toBe(true);
expect(equalTest({ a: 1, b: 1 }, { a: 1, b: 1, c: 5 })).toBe(true);
expect(equalTest({ a: 1, b: 1, c: 4 }, { a: 1, b: 1, c: 5 })).toBe(true);
expect(equalTest({ a: 1, b: 1 }, { a: 2, b: 2 })).toBe(false);
expect(equalTest({ a: 1, b: 1 }, { a: 2, b: 1 })).toBe(false);
expect(equalTest(undefined, { a: 2, b: 1 })).toBe(false);
expect(equalTest({ a: 1, b: 1 }, undefined)).toBe(false);
});
test('equalTRBL', () => {
expect(equalTRBL({ t: 0, r: 0, b: 0, l: 0 }, { t: 0, r: 0, b: 0, l: 0 })).toBe(true);
expect(equalTRBL({ t: 0, r: 0, b: 0, l: 0 }, { t: 0, r: 0, b: 0, l: 1 })).toBe(false);
});
test('equalWH', () => {
expect(equalWH({ w: 0, h: 0 }, { w: 0, h: 0 })).toBe(true);
expect(equalWH({ w: 0, h: 0 }, { w: 0, h: 1 })).toBe(false);
});
test('equalXY', () => {
expect(equalXY({ x: 0, y: 0 }, { x: 0, y: 0 })).toBe(true);
expect(equalXY({ x: 0, y: 0 }, { x: 0, y: 1 })).toBe(false);
});
});
@@ -1,7 +1,8 @@
import 'overlayscrollbars.scss';
import { createDOM, appendChildren } from 'support';
import { getEnvironment } from 'environment';
const envInstance = getEnvironment();
document.body.textContent = JSON.stringify(envInstance);
appendChildren(document.body, createDOM(`<div>${JSON.stringify(envInstance)}</div>`)[0]);
export { envInstance };
@@ -1,4 +1 @@
<div id="a"></div>
<div id="b"></div>
<div id="c"></div>
<div id="d"></div>
hi
@@ -158,7 +158,7 @@ startBtn?.addEventListener('click', start);
createSizeObserver(
targetElm as HTMLElement,
(direction?: boolean) => {
(direction?: 'ltr' | 'rtl') => {
if (direction) {
directionIterations += 1;
} else {
@@ -0,0 +1,7 @@
import 'overlayscrollbars.scss';
import './index.scss';
import { createStructureLifecycle } from 'overlayscrollbars/lifecycles/StructureLifecycle';
const targetElm = document.querySelector('#target') as HTMLElement;
const structureLifecycle = createStructureLifecycle(targetElm);
@@ -0,0 +1,53 @@
<div id="controls">
<label for="height">height</label>
<select name="height" id="height">
<option value="heightAuto">auto</option>
<option value="heightHundred">100%</option>
<option value="height200">200px</option>
</select>
<label for="width">width</label>
<select name="width" id="width">
<option value="widthAuto">auto</option>
<option value="widthHundred">100%</option>
<option value="width200">200px</option>
</select>
<label for="padding">padding</label>
<select name="padding" id="padding">
<option value="padding0">0</option>
<option value="padding10">10px</option>
<option value="padding50">50px</option>
</select>
<label for="border">border</label>
<select name="border" id="border">
<option value="border2">2px</option>
<option value="border10">10px</option>
<option value="border0">0</option>
</select>
<label for="boxSizing">boxSizing</label>
<select name="boxSizing" id="boxSizing">
<option value="boxSizingBorderBox">border-box</option>
<option value="boxSizingContentBox">content-box</option>
</select>
<label for="display">display</label>
<select name="display" id="display">
<option value="displayBlock">block</option>
<option value="displayNone">none</option>
</select>
<label for="direction">direction</label>
<select name="direction" id="direction">
<option value="directionLTR">ltr</option>
<option value="directionRTL">rtl</option>
</select>
<button id="start">start</button>
<span>Detected resizes: <span id="resizes">0</span></span>
</div>
<div id="stage">
<div>
<div id="target">
<div id="resize">Resize</div>
<div id="hundred">100%</div>
<div id="end">End</div>
</div>
</div>
</div>
@@ -0,0 +1,137 @@
body {
display: flex;
flex-direction: column;
}
#controls {
flex: none;
}
#stage {
flex: auto;
position: relative;
& > div {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: lightgoldenrodyellow;
}
}
#canvas > div {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
#target {
overflow: hidden;
resize: both;
position: relative;
border: 2px solid red;
min-height: 100px;
min-width: 200px;
max-height: 300px;
max-width: 320px;
}
#resize {
overflow: hidden;
resize: both;
background: blue;
border: 1px solid black;
padding: 10px;
}
#hundred {
height: 100%;
background: purple;
border: 1px solid black;
padding: 10px;
}
#end {
position: relative;
background: green;
border: 1px solid black;
padding: 10px;
margin: 10px;
}
#end::before {
content: '';
position: absolute;
display: block;
top: -11px;
right: -11px;
bottom: -11px;
left: -11px;
background: green;
z-index: -1;
opacity: 0.5;
}
.padding0 {
padding: 0;
}
.padding10 {
padding: 10px;
}
.padding50 {
padding: 50px;
}
.border2 {
border: 2px solid red;
}
.border10 {
border: 10px solid red;
}
.border0 {
border: none;
}
.heightAuto {
height: auto;
}
.height200 {
height: 200px;
}
.heightHundred {
height: 100%;
}
.widthAuto {
width: auto;
float: left;
}
.width200 {
width: 200px;
}
.widthHundred {
width: 100%;
}
.boxSizingBorderBox {
box-sizing: border-box;
}
.boxSizingContentBox {
box-sizing: content-box;
}
.displayNone {
display: none;
}
.displayBlock {
display: block;
}
.directionltr {
direction: ltr;
}
.directionRTL {
direction: rtl;
}
@@ -0,0 +1,15 @@
import { Environment } from 'environment';
import url from './.build/build.html';
describe('StructureLifecycle', () => {
beforeAll(async () => {
await page.goto(url);
});
it('should be titled "Environment"', async () => {
// @ts-ignore
const a: Environment = await page.evaluate(() => window.Environment.envInstance);
console.log(a);
await expect(page.title()).resolves.toMatch('Environment');
});
});