mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-05-22 05:34:06 +03:00
add cache support and start structure lifecycle
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user