mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-05-25 12:14:07 +03:00
overflow lifecycle
This commit is contained in:
@@ -19,7 +19,7 @@ const createAutoUpdateLoop = (): AutoUpdateLoop => {
|
||||
const updateLoopInterval = () => {
|
||||
loopInterval = isEmptyArray(intervals) ? defaultLoopInterval : Math.min.apply(null, intervals);
|
||||
};
|
||||
const updateTimeCache = createCache<number, number>((ctx) => ctx || performance.now(), {
|
||||
const { _update: updateTimeCache } = createCache<number, number | undefined>((ctx) => ctx || performance.now(), {
|
||||
_initialValue: performance.now(),
|
||||
_equal: (currTime, newTime) => {
|
||||
const delta = newTime! - currTime!;
|
||||
|
||||
@@ -6,6 +6,7 @@ export const classNameHost = 'os-host';
|
||||
export const classNamePadding = 'os-padding';
|
||||
export const classNameViewport = 'os-viewport';
|
||||
export const classNameContent = 'os-content';
|
||||
export const classNameContentArrange = `${classNameContent}-arrange`;
|
||||
export const classNameViewportScrollbarStyling = `${classNameViewport}-scrollbar-styled`;
|
||||
|
||||
export const classNameSizeObserver = 'os-size-observer';
|
||||
|
||||
@@ -122,17 +122,18 @@ const createEnvironment = (): Environment => {
|
||||
const envChildElm = envElm.firstChild as HTMLElement;
|
||||
|
||||
const onChangedListener: Set<OnEnvironmentChanged> = new Set();
|
||||
const nativeScrollBarSize = getNativeScrollbarSize(body, envElm);
|
||||
const nativeScrollbarSize = getNativeScrollbarSize(body, envElm);
|
||||
const nativeScrollbarStyling = false; //getNativeScrollbarStyling(envElm); TODO: Re-enable
|
||||
const nativeScrollbarIsOverlaid = {
|
||||
x: nativeScrollBarSize.x === 0,
|
||||
y: nativeScrollBarSize.y === 0,
|
||||
x: nativeScrollbarSize.x === 0,
|
||||
y: nativeScrollbarSize.y === 0,
|
||||
};
|
||||
|
||||
const env: Environment = {
|
||||
_autoUpdateLoop: false,
|
||||
_nativeScrollbarSize: nativeScrollBarSize,
|
||||
_nativeScrollbarSize: nativeScrollbarSize,
|
||||
_nativeScrollbarIsOverlaid: nativeScrollbarIsOverlaid,
|
||||
_nativeScrollbarStyling: getNativeScrollbarStyling(envElm),
|
||||
_nativeScrollbarStyling: nativeScrollbarStyling,
|
||||
_rtlScrollBehavior: getRtlScrollBehavior(envElm, envChildElm),
|
||||
_flexboxGlue: getFlexboxGlue(envElm, envChildElm),
|
||||
_addListener(listener: OnEnvironmentChanged): void {
|
||||
@@ -144,13 +145,12 @@ const createEnvironment = (): Environment => {
|
||||
};
|
||||
|
||||
removeAttr(envElm, 'style');
|
||||
removeAttr(envElm, 'class');
|
||||
removeElements(envElm);
|
||||
|
||||
if (!nativeScrollbarIsOverlaid.x || !nativeScrollbarIsOverlaid.y) {
|
||||
if (!nativeScrollbarStyling && (!nativeScrollbarIsOverlaid.x || !nativeScrollbarIsOverlaid.y)) {
|
||||
let size = windowSize();
|
||||
let dpr = getWindowDPR();
|
||||
let scrollbarSize = nativeScrollBarSize;
|
||||
let scrollbarSize = nativeScrollbarSize;
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (onChangedListener.size) {
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import {
|
||||
Cache,
|
||||
OptionsValidated,
|
||||
OptionsWithOptionsTemplate,
|
||||
transformOptions,
|
||||
validateOptions,
|
||||
assignDeep,
|
||||
hasOwnProperty,
|
||||
isEmptyObject,
|
||||
} from 'support';
|
||||
import { PlainObject } from 'typings';
|
||||
|
||||
interface LifecycleBaseUpdateHints<O> {
|
||||
_force?: boolean;
|
||||
_changedOptions?: OptionsValidated<O>;
|
||||
}
|
||||
|
||||
export interface LifecycleBase<O extends PlainObject> {
|
||||
_options(newOptions?: O): O;
|
||||
_update(force?: boolean): void;
|
||||
}
|
||||
|
||||
export interface Lifecycle<T extends PlainObject> extends LifecycleBase<T> {
|
||||
_destruct(): void;
|
||||
_onSizeChanged?(): void;
|
||||
_onDirectionChanged?(directionCache: Cache<boolean>): void;
|
||||
_onTrinsicChanged?(widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>): void;
|
||||
}
|
||||
|
||||
export interface LifecycleOptionInfo<T> {
|
||||
_value: T;
|
||||
_changed: boolean;
|
||||
}
|
||||
|
||||
export type LifecycleCheckOption = <T>(path: string) => LifecycleOptionInfo<T>;
|
||||
|
||||
const getPropByPath = <T>(obj: any, path: string): T =>
|
||||
obj && path.split('.').reduce((o, prop) => (o && hasOwnProperty(o, prop) ? o[prop] : undefined), obj);
|
||||
|
||||
/**
|
||||
* Creates a object which can be seen as the base of a lifecycle because it provides all the tools to manage a lifecycle and its options, cache and base functions.
|
||||
* @param defaultOptionsWithTemplate A object which describes the options and the default options of the lifecycle.
|
||||
* @param initialOptions The initialOptions for the lifecylce. (Can be undefined)
|
||||
* @param updateFunction The update function where cache and options updates are handled. Has two arguments which are the changedOptions and the changedCache objects.
|
||||
*/
|
||||
export const createLifecycleBase = <O>(
|
||||
defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<O>>,
|
||||
initialOptions: O | undefined,
|
||||
updateFunction: (force: boolean, checkOption: LifecycleCheckOption) => any
|
||||
): LifecycleBase<O> => {
|
||||
const { _template: optionsTemplate, _options: defaultOptions } = transformOptions<Required<O>>(defaultOptionsWithTemplate);
|
||||
const options: Required<O> = assignDeep(
|
||||
{},
|
||||
defaultOptions,
|
||||
validateOptions<O>(initialOptions || ({} as O), optionsTemplate, null, true)._validated
|
||||
);
|
||||
|
||||
const update = (hints: LifecycleBaseUpdateHints<O>) => {
|
||||
const { _force, _changedOptions } = hints;
|
||||
const checkOption: LifecycleCheckOption = (path) => ({
|
||||
_value: getPropByPath(options, path),
|
||||
_changed: _force || getPropByPath(_changedOptions, path) !== undefined,
|
||||
});
|
||||
updateFunction(!!_force, checkOption);
|
||||
};
|
||||
|
||||
update({ _force: true });
|
||||
|
||||
return {
|
||||
_options(newOptions?: O) {
|
||||
if (newOptions) {
|
||||
const { _validated: _changedOptions } = validateOptions(newOptions, optionsTemplate, options, true);
|
||||
|
||||
if (!isEmptyObject(_changedOptions)) {
|
||||
assignDeep(options, _changedOptions);
|
||||
update({ _changedOptions });
|
||||
}
|
||||
}
|
||||
return options;
|
||||
},
|
||||
_update: (_force?: boolean) => {
|
||||
update({ _force });
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { CacheValues, each, push, validateOptions, assignDeep, isEmptyObject, OptionsValidated } from 'support';
|
||||
import { Options } from 'options';
|
||||
import { getEnvironment, Environment } from 'environment';
|
||||
import { StructureSetup } from 'setups/structureSetup';
|
||||
import { createStructureLifecycle } from 'lifecycles/structureLifecycle';
|
||||
import { createOverflowLifecycle } from 'lifecycles/overflowLifecycle';
|
||||
import { LifecycleUpdateFunction, LifecycleUpdateHints } from 'lifecycles/lifecycleUpdateFunction';
|
||||
import { createSizeObserver } from 'observers/sizeObserver';
|
||||
import { createTrinsicObserver } from 'observers/trinsicObserver';
|
||||
import { createDOMObserver } from 'observers/domObserver';
|
||||
|
||||
export interface LifecycleHubInstance {
|
||||
_update(changedOptions?: OptionsValidated<Options> | null, force?: boolean): void;
|
||||
_destroy(): void;
|
||||
}
|
||||
|
||||
export interface LifecycleHub {
|
||||
_options: Options;
|
||||
_structureSetup: StructureSetup;
|
||||
}
|
||||
|
||||
const attrs = ['id', 'class', 'style', 'open'];
|
||||
const directionIsRTLCacheValuesFallback: CacheValues<boolean> = {
|
||||
_value: false,
|
||||
_previous: false,
|
||||
_changed: false,
|
||||
};
|
||||
const heightIntrinsicCacheValuesFallback: CacheValues<boolean> = {
|
||||
_value: false,
|
||||
_previous: false,
|
||||
_changed: false,
|
||||
};
|
||||
|
||||
export const createLifecycleHub = (options: Options, structureSetup: StructureSetup): LifecycleHubInstance => {
|
||||
const { _host, _viewport, _content } = structureSetup._targetObj;
|
||||
const environment: Environment = getEnvironment();
|
||||
const lifecycles: LifecycleUpdateFunction[] = [];
|
||||
const instance: LifecycleHub = {
|
||||
_options: options,
|
||||
_structureSetup: structureSetup,
|
||||
};
|
||||
|
||||
// push(lifecycles, createStructureLifecycle(instance));
|
||||
push(lifecycles, createOverflowLifecycle(instance));
|
||||
|
||||
const runLifecycles = (updateHints?: Partial<LifecycleUpdateHints> | null, changedOptions?: OptionsValidated<Options> | null, force?: boolean) => {
|
||||
let { _directionIsRTL, _heightIntrinsic, _sizeChanged = force || false, _hostMutation = force || false, _contentMutation = force || false } =
|
||||
updateHints || {};
|
||||
const finalDirectionIsRTL =
|
||||
_directionIsRTL || (sizeObserver ? sizeObserver._getCurrentCacheValues(force)._directionIsRTL : directionIsRTLCacheValuesFallback);
|
||||
const finalHeightIntrinsic =
|
||||
_heightIntrinsic || (trinsicObserver ? trinsicObserver._getCurrentCacheValues(force)._heightIntrinsic : heightIntrinsicCacheValuesFallback);
|
||||
|
||||
each(lifecycles, (lifecycle) => {
|
||||
const { _sizeChanged: adaptiveSizeChanged, _hostMutation: adaptiveHostMutation, _contentMutation: adaptiveContentMutation } = lifecycle(
|
||||
{
|
||||
_directionIsRTL: finalDirectionIsRTL,
|
||||
_heightIntrinsic: finalHeightIntrinsic,
|
||||
_sizeChanged,
|
||||
_hostMutation,
|
||||
_contentMutation,
|
||||
},
|
||||
changedOptions,
|
||||
force
|
||||
);
|
||||
|
||||
_sizeChanged = adaptiveSizeChanged || _sizeChanged;
|
||||
_hostMutation = adaptiveHostMutation || _hostMutation;
|
||||
_contentMutation = adaptiveContentMutation || _contentMutation;
|
||||
});
|
||||
};
|
||||
|
||||
const onSizeChanged = (directionIsRTL?: CacheValues<boolean>) => {
|
||||
const sizeChanged = !directionIsRTL;
|
||||
runLifecycles({
|
||||
_directionIsRTL: directionIsRTL,
|
||||
_sizeChanged: sizeChanged,
|
||||
});
|
||||
};
|
||||
const onTrinsicChanged = (heightIntrinsic: CacheValues<boolean>) => {
|
||||
runLifecycles({
|
||||
_heightIntrinsic: heightIntrinsic,
|
||||
});
|
||||
};
|
||||
const onHostMutation = () => {
|
||||
// TODO: rAF only here because IE
|
||||
requestAnimationFrame(() => {
|
||||
runLifecycles({
|
||||
_hostMutation: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
const onContentMutation = () => {
|
||||
// TODO: rAF only here because IE
|
||||
requestAnimationFrame(() => {
|
||||
runLifecycles({
|
||||
_contentMutation: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const sizeObserver = createSizeObserver(_host, onSizeChanged, { _appear: true, _direction: true });
|
||||
const trinsicObserver = createTrinsicObserver(_host, onTrinsicChanged);
|
||||
const hostMutationObserver = createDOMObserver(_host, onHostMutation, {
|
||||
_styleChangingAttributes: attrs,
|
||||
_attributes: attrs,
|
||||
});
|
||||
const contentMutationObserver = createDOMObserver(_content || _viewport, onContentMutation, {
|
||||
_observeContent: true,
|
||||
_styleChangingAttributes: attrs,
|
||||
_attributes: attrs,
|
||||
_eventContentChange: options!.updating!.elementEvents as [string, string][],
|
||||
/*
|
||||
_nestedTargetSelector: hostSelector,
|
||||
_ignoreContentChange: (mutation, isNestedTarget) => {
|
||||
const { target, attributeName } = mutation;
|
||||
return isNestedTarget ? false : attributeName ? liesBetween(target as Element, hostSelector, '.content') : false;
|
||||
},
|
||||
_ignoreTargetAttrChange: (target, attrName, oldValue, newValue) => {
|
||||
if (attrName === 'class' && oldValue && newValue) {
|
||||
const diff = diffClass(oldValue, newValue);
|
||||
const ignore = diff.length === 1 && diff[0].startsWith(ignorePrefix);
|
||||
return ignore;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
*/
|
||||
});
|
||||
|
||||
const updateAll = (changedOptions?: OptionsValidated<Options> | null, force?: boolean) => {
|
||||
runLifecycles(null, changedOptions, force);
|
||||
};
|
||||
const envUpdateListener = updateAll.bind(null, null, true);
|
||||
environment._addListener(envUpdateListener);
|
||||
|
||||
console.log('flexboxglue', environment._flexboxGlue);
|
||||
|
||||
return {
|
||||
_update: updateAll,
|
||||
_destroy() {
|
||||
environment._removeListener(envUpdateListener);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { CacheValues, OptionsValidated, hasOwnProperty } from 'support';
|
||||
import { Options } from 'options';
|
||||
import { LifecycleHub } from 'lifecycles/lifecycleHub';
|
||||
|
||||
export interface LifecycleAdaptiveUpdateHints {
|
||||
_sizeChanged: boolean;
|
||||
_hostMutation: boolean;
|
||||
_contentMutation: boolean;
|
||||
}
|
||||
|
||||
export interface LifecycleUpdateHints extends LifecycleAdaptiveUpdateHints {
|
||||
_directionIsRTL: CacheValues<boolean>;
|
||||
_heightIntrinsic: CacheValues<boolean>;
|
||||
}
|
||||
|
||||
export type LifecycleUpdateFunction = (
|
||||
updateHints: LifecycleUpdateHints,
|
||||
changedOptions?: OptionsValidated<Options> | null,
|
||||
force?: boolean
|
||||
) => Partial<LifecycleAdaptiveUpdateHints>;
|
||||
|
||||
export interface LifecycleOptionInfo<T> {
|
||||
readonly _value: T;
|
||||
_changed: boolean;
|
||||
}
|
||||
|
||||
export type LifecycleCheckOption = <T>(path: string) => LifecycleOptionInfo<T>;
|
||||
|
||||
const getPropByPath = <T>(obj: any, path: string): T =>
|
||||
obj && path.split('.').reduce((o, prop) => (o && hasOwnProperty(o, prop) ? o[prop] : undefined), obj);
|
||||
|
||||
/**
|
||||
* Creates a update function for a lifecycle.
|
||||
* @param lifecycleHub The LifecycleHub which is managing this lifecylce.
|
||||
* @param updateFunction The update function where cache and options updates are handled. Has two arguments which are the changedOptions and the changedCache objects.
|
||||
*/
|
||||
export const createLifecycleUpdateFunction = (
|
||||
lifecycleHub: LifecycleHub,
|
||||
updateFunction: (
|
||||
force: boolean,
|
||||
updateHints: LifecycleUpdateHints,
|
||||
checkOption: LifecycleCheckOption
|
||||
) => Partial<LifecycleAdaptiveUpdateHints> | void
|
||||
): LifecycleUpdateFunction => {
|
||||
return (updateHints: LifecycleUpdateHints, changedOptions?: OptionsValidated<Options> | null, force?: boolean) => {
|
||||
const checkOption: LifecycleCheckOption = (path) => ({
|
||||
_value: getPropByPath(lifecycleHub._options, path),
|
||||
_changed: force || getPropByPath(changedOptions, path) !== undefined,
|
||||
});
|
||||
return updateFunction(!!force, updateHints, checkOption) || {};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
import { createCache, WH, XY, equalXY, style, scrollSize, offsetSize, CacheValues, equalWH, scrollLeft, scrollTop } from 'support';
|
||||
import { createLifecycleUpdateFunction, LifecycleUpdateFunction } from 'lifecycles/lifecycleUpdateFunction';
|
||||
import { LifecycleHub } from 'lifecycles/lifecycleHub';
|
||||
import { getEnvironment } from 'environment';
|
||||
import { OverflowBehavior } from 'options';
|
||||
import { PlainObject } from 'typings';
|
||||
|
||||
const overlaidScrollbarsHideOffset = 42;
|
||||
const overlaidScrollbarsHideBorderStyle = `${overlaidScrollbarsHideOffset}px solid transparent`;
|
||||
interface OverflowAmountCacheContext {
|
||||
_contentScrollSize: WH<number>;
|
||||
_viewportSize: WH<number>;
|
||||
}
|
||||
|
||||
export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): LifecycleUpdateFunction => {
|
||||
const { _host, _padding, _viewport, _content, _contentArrange } = lifecycleHub._structureSetup._targetObj;
|
||||
const { _update: updateContentScrollSizeCache, _current: getCurrentContentScrollSizeCache } = createCache<WH<number>>(
|
||||
() => scrollSize(_content || _viewport),
|
||||
{ _equal: equalWH }
|
||||
);
|
||||
const { _update: updateOverflowAmountCache, _current: getCurrentOverflowAmountCache } = createCache<XY<number>, OverflowAmountCacheContext>(
|
||||
(ctx) => ({
|
||||
x: Math.max(0, Math.round((ctx._contentScrollSize.w - ctx._viewportSize.w) * 100) / 100),
|
||||
y: Math.max(0, Math.round((ctx._contentScrollSize.h - ctx._viewportSize.h) * 100) / 100),
|
||||
}),
|
||||
{ _equal: equalXY }
|
||||
);
|
||||
|
||||
const setViewportOverflowStyle = (horizontal: boolean, amount: number, behavior: OverflowBehavior, styleObj: PlainObject) => {
|
||||
const overflowKey = horizontal ? 'overflowX' : 'overflowY';
|
||||
//const scrollMaxKey = horizontal ? 'scrollLeftMax' : 'scrollTopMax';
|
||||
const behaviorIsScroll = behavior === 'scroll';
|
||||
const behaviorIsVisibleScroll = behavior === 'visible-scroll';
|
||||
const hideOverflow = behaviorIsScroll || behavior === 'hidden';
|
||||
//const scrollMax = _viewport[scrollMaxKey];
|
||||
//const scrollMaxOverflow = isNumber(scrollMax) ? scrollMax > 0 : true;
|
||||
const applyStyle = amount > 0 && hideOverflow;
|
||||
|
||||
if (applyStyle) {
|
||||
styleObj[overflowKey] = behavior;
|
||||
}
|
||||
|
||||
return {
|
||||
_visible: !applyStyle,
|
||||
_behavior: behaviorIsVisibleScroll ? 'scroll' : 'hidden',
|
||||
};
|
||||
};
|
||||
|
||||
const hideNativeScrollbars = (
|
||||
contentScrollSize: WH<number>,
|
||||
adjustFlexboxGlue: boolean,
|
||||
directionIsRTL: boolean,
|
||||
heightIntrinsic: boolean,
|
||||
viewportStyleObj: PlainObject,
|
||||
contentStyleObj: PlainObject
|
||||
) => {
|
||||
const { _nativeScrollbarSize, _nativeScrollbarIsOverlaid, _nativeScrollbarStyling } = getEnvironment();
|
||||
const scrollX = viewportStyleObj.overflowX === 'scroll';
|
||||
const scrollY = viewportStyleObj.overflowY === 'scroll';
|
||||
const horizontalMarginKey = directionIsRTL ? 'marginLeft' : 'marginRight';
|
||||
const horizontalBorderKey = directionIsRTL ? 'borderLeft' : 'borderRight';
|
||||
const scrollXY = scrollY && scrollX;
|
||||
const hideOffset = _content ? overlaidScrollbarsHideOffset : 0;
|
||||
const offset = {
|
||||
x: _nativeScrollbarIsOverlaid.x ? hideOffset : _nativeScrollbarSize.x,
|
||||
y: _nativeScrollbarIsOverlaid.y ? hideOffset : _nativeScrollbarSize.y,
|
||||
};
|
||||
|
||||
if (!_nativeScrollbarStyling) {
|
||||
if (scrollX) {
|
||||
viewportStyleObj.marginBottom = `-${offset.x}px`;
|
||||
|
||||
if (_nativeScrollbarIsOverlaid.x && hideOffset) {
|
||||
contentStyleObj.borderBottom = overlaidScrollbarsHideBorderStyle;
|
||||
}
|
||||
}
|
||||
if (scrollY) {
|
||||
viewportStyleObj.maxWidth = `calc(100% + ${offset.y}px)`;
|
||||
viewportStyleObj[horizontalMarginKey] = `-${offset.y}px`;
|
||||
|
||||
if (_nativeScrollbarIsOverlaid.y && hideOffset) {
|
||||
contentStyleObj[horizontalBorderKey] = overlaidScrollbarsHideBorderStyle;
|
||||
}
|
||||
}
|
||||
|
||||
if (hideOffset && (offset.x || offset.y)) {
|
||||
style(_contentArrange, {
|
||||
width: scrollXY ? `${hideOffset + contentScrollSize.w}px` : '',
|
||||
height: scrollXY ? `${hideOffset + contentScrollSize.h}px` : '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (adjustFlexboxGlue) {
|
||||
const offsetLeft = scrollLeft(_viewport);
|
||||
const offsetTop = scrollTop(_viewport);
|
||||
|
||||
style(_viewport, {
|
||||
maxHeight: '',
|
||||
});
|
||||
|
||||
if (heightIntrinsic) {
|
||||
style(_viewport, {
|
||||
maxHeight: `${_host.clientHeight + (scrollX ? offset.x : 0)}px`,
|
||||
});
|
||||
}
|
||||
|
||||
scrollLeft(_viewport, offsetLeft);
|
||||
scrollTop(_viewport, offsetTop);
|
||||
}
|
||||
};
|
||||
|
||||
return createLifecycleUpdateFunction(lifecycleHub, (force, updateHints, checkOption) => {
|
||||
const { _directionIsRTL, _heightIntrinsic, _sizeChanged, _hostMutation, _contentMutation } = updateHints;
|
||||
const { _flexboxGlue, _nativeScrollbarStyling } = getEnvironment();
|
||||
const adjustFlexboxGlue = !_flexboxGlue && (_sizeChanged || _contentMutation || _hostMutation);
|
||||
let overflowAmuntCache: CacheValues<XY<number>> = getCurrentOverflowAmountCache();
|
||||
let contentScrollSizeCache: CacheValues<WH<number>> = getCurrentContentScrollSizeCache();
|
||||
|
||||
if (_sizeChanged || _contentMutation) {
|
||||
const viewportOffsetSize = offsetSize(_padding);
|
||||
const contentClientSize = offsetSize(_content || _viewport);
|
||||
const contentArrangeOffsetSize = offsetSize(_contentArrange);
|
||||
|
||||
contentScrollSizeCache = updateContentScrollSizeCache(force);
|
||||
const { _value: contentScrollSize } = contentScrollSizeCache;
|
||||
overflowAmuntCache = updateOverflowAmountCache(force, {
|
||||
_contentScrollSize: {
|
||||
w: Math.max(contentScrollSize!.w, contentArrangeOffsetSize.w),
|
||||
h: Math.max(contentScrollSize!.h, contentArrangeOffsetSize.h),
|
||||
},
|
||||
_viewportSize: {
|
||||
w: viewportOffsetSize.w + Math.max(0, contentClientSize.w - contentScrollSize!.w),
|
||||
h: viewportOffsetSize.h + Math.max(0, contentClientSize.h - contentScrollSize!.h),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { _value: directionIsRTL, _changed: directionChanged } = _directionIsRTL;
|
||||
const { _value: contentScrollSize, _changed: contentScrollSizeChanged } = contentScrollSizeCache;
|
||||
const { _value: overflowAmount, _changed: overflowAmountChanged } = overflowAmuntCache;
|
||||
const { _value: overflow, _changed: overflowChanged } = checkOption<{
|
||||
x: OverflowBehavior;
|
||||
y: OverflowBehavior;
|
||||
}>('overflow');
|
||||
const adjustDirection = directionChanged && !_nativeScrollbarStyling;
|
||||
|
||||
if (contentScrollSizeChanged || overflowAmountChanged || overflowChanged || adjustDirection || adjustFlexboxGlue) {
|
||||
const viewportStyle: PlainObject = {
|
||||
overflowY: '',
|
||||
overflowX: '',
|
||||
marginTop: '',
|
||||
marginRight: '',
|
||||
marginBottom: '',
|
||||
marginLeft: '',
|
||||
maxWidth: '',
|
||||
};
|
||||
const contentStyle: PlainObject = {
|
||||
borderTop: '',
|
||||
borderRight: '',
|
||||
borderBottom: '',
|
||||
borderLeft: '',
|
||||
};
|
||||
|
||||
const { _visible: xVisible, _behavior: xVisibleBehavior } = setViewportOverflowStyle(true, overflowAmount!.x, overflow.x, viewportStyle);
|
||||
const { _visible: yVisible, _behavior: yVisibleBehavior } = setViewportOverflowStyle(false, overflowAmount!.y, overflow.y, viewportStyle);
|
||||
|
||||
if (xVisible && !yVisible) {
|
||||
viewportStyle.overflowX = xVisibleBehavior;
|
||||
}
|
||||
if (yVisible && !xVisible) {
|
||||
viewportStyle.overflowY = yVisibleBehavior;
|
||||
}
|
||||
|
||||
hideNativeScrollbars(contentScrollSize!, adjustFlexboxGlue, directionIsRTL!, !!_heightIntrinsic._value, viewportStyle, contentStyle);
|
||||
|
||||
// TODO: enlargen viewport if div too small for firefox scrollbar hiding behavior
|
||||
// TODO: Test without content
|
||||
// TODO: Test without padding
|
||||
|
||||
style(_viewport, viewportStyle);
|
||||
style(_content, contentStyle);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
Cache,
|
||||
CacheValues,
|
||||
cssProperty,
|
||||
runEach,
|
||||
createCache,
|
||||
@@ -9,43 +9,19 @@ import {
|
||||
XY,
|
||||
equalTRBL,
|
||||
equalXY,
|
||||
optionsTemplateTypes as oTypes,
|
||||
OptionsTemplateValue,
|
||||
style,
|
||||
OptionsWithOptionsTemplate,
|
||||
scrollSize,
|
||||
offsetSize,
|
||||
} from 'support';
|
||||
import { PreparedOSTargetObject } from 'setups/structureSetup';
|
||||
import { createLifecycleBase, Lifecycle } from 'lifecycles/lifecycleBase';
|
||||
import { createLifecycleUpdateFunction, Lifecycle } from 'lifecycles/lifecycleUpdateFunction';
|
||||
import { LifecycleHub } from 'lifecycles/lifecycleHub';
|
||||
import { getEnvironment, Environment } from 'environment';
|
||||
|
||||
export type OverflowBehavior = 'hidden' | 'scroll' | 'visible-hidden' | 'visible-scroll';
|
||||
export interface StructureLifecycleOptions {
|
||||
paddingAbsolute: boolean;
|
||||
overflowBehavior?: {
|
||||
x?: OverflowBehavior;
|
||||
y?: OverflowBehavior;
|
||||
};
|
||||
}
|
||||
|
||||
const overflowBehaviorAllowedValues: OptionsTemplateValue<OverflowBehavior> = 'visible-hidden visible-scroll scroll hidden';
|
||||
const defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<StructureLifecycleOptions>> = {
|
||||
paddingAbsolute: [false, oTypes.boolean],
|
||||
overflowBehavior: {
|
||||
x: ['scroll', overflowBehaviorAllowedValues],
|
||||
y: ['scroll', overflowBehaviorAllowedValues],
|
||||
},
|
||||
};
|
||||
|
||||
const cssMarginEnd = cssProperty('margin-inline-end');
|
||||
const cssBorderEnd = cssProperty('border-inline-end');
|
||||
|
||||
export const createStructureLifecycle = (
|
||||
target: PreparedOSTargetObject,
|
||||
initialOptions?: StructureLifecycleOptions
|
||||
): Lifecycle<StructureLifecycleOptions> => {
|
||||
const { _host, _padding, _viewport, _content } = target;
|
||||
export const createStructureLifecycle = (lifecycleHub: LifecycleHub): Lifecycle => {
|
||||
const { _host, _padding, _viewport, _content } = lifecycleHub._structureSetup._targetObj;
|
||||
const destructFns: (() => any)[] = [];
|
||||
const env: Environment = getEnvironment();
|
||||
const scrollbarsOverlaid = env._nativeScrollbarIsOverlaid;
|
||||
@@ -54,8 +30,8 @@ export const createStructureLifecycle = (
|
||||
// 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 updatePaddingCache = createCache(() => topRightBottomLeft(_host, 'padding'), { _equal: equalTRBL });
|
||||
const updateOverflowAmountCache = createCache<XY<number>, { _contentScrollSize: WH<number>; _viewportSize: WH<number> }>(
|
||||
const { _update: updatePaddingCache } = createCache(() => topRightBottomLeft(_host, 'padding'), { _equal: equalTRBL });
|
||||
const { _update: updateOverflowAmountCache } = createCache<XY<number>, { _contentScrollSize: WH<number>; _viewportSize: WH<number> }>(
|
||||
(ctx) => ({
|
||||
x: Math.max(0, Math.round((ctx!._contentScrollSize.w - ctx!._viewportSize.w) * 100) / 100),
|
||||
y: Math.max(0, Math.round((ctx!._contentScrollSize.h - ctx!._viewportSize.h) * 100) / 100),
|
||||
@@ -63,7 +39,7 @@ export const createStructureLifecycle = (
|
||||
{ _equal: equalXY }
|
||||
);
|
||||
|
||||
const { _options, _update } = createLifecycleBase<StructureLifecycleOptions>(defaultOptionsWithTemplate, initialOptions, (force, checkOption) => {
|
||||
const _update = createLifecycleUpdateFunction(lifecycleHub, (force, checkOption) => {
|
||||
const { _value: paddingAbsolute, _changed: paddingAbsoluteChanged } = checkOption('paddingAbsolute');
|
||||
const { _value: padding, _changed: paddingChanged } = updatePaddingCache(force);
|
||||
|
||||
@@ -148,15 +124,14 @@ export const createStructureLifecycle = (
|
||||
const onSizeChanged = () => {
|
||||
_update();
|
||||
};
|
||||
const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>) => {
|
||||
const { _changed, _value } = heightIntrinsicCache;
|
||||
const onTrinsicChanged = (heightIntrinsic: CacheValues<boolean>) => {
|
||||
const { _changed, _value } = heightIntrinsic;
|
||||
if (_changed) {
|
||||
style(_content, { height: _value ? 'auto' : '100%' });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
_options,
|
||||
_update,
|
||||
_onSizeChanged: onSizeChanged,
|
||||
_onTrinsicChanged: onTrinsicChanged,
|
||||
|
||||
@@ -149,7 +149,7 @@ export const createDOMObserver = (
|
||||
if (isConnected) {
|
||||
callback([], false, true);
|
||||
}
|
||||
}, 80)
|
||||
}, 84)
|
||||
);
|
||||
|
||||
// MutationObserver
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Cache,
|
||||
CacheValues,
|
||||
createCache,
|
||||
createDOM,
|
||||
style,
|
||||
@@ -34,6 +35,21 @@ import {
|
||||
classNameSizeObserverListenerItemFinal,
|
||||
} from 'classnames';
|
||||
|
||||
interface SizeObserverEntry {
|
||||
contentRect: DOMRectReadOnly;
|
||||
}
|
||||
|
||||
export type SizeObserverOptions = { _direction?: boolean; _appear?: boolean };
|
||||
|
||||
export interface SizeObserver {
|
||||
_destroy(): void;
|
||||
_getCurrentCacheValues(
|
||||
force?: boolean
|
||||
): {
|
||||
_directionIsRTL: CacheValues<boolean>;
|
||||
};
|
||||
}
|
||||
|
||||
const animationStartEventName = 'animationstart';
|
||||
const scrollEventName = 'scroll';
|
||||
const scrollAmount = 3333333;
|
||||
@@ -51,21 +67,17 @@ const directionIsRTL = (elm: HTMLElement): boolean => {
|
||||
};
|
||||
const domRectHasDimensions = (rect?: DOMRectReadOnly) => rect && (rect.height || rect.width);
|
||||
|
||||
interface SizeObserverEntry {
|
||||
contentRect: DOMRectReadOnly;
|
||||
}
|
||||
export type SizeObserverOptions = { _direction?: boolean; _appear?: boolean };
|
||||
export const createSizeObserver = (
|
||||
target: HTMLElement,
|
||||
onSizeChangedCallback: (directionIsRTLCache?: Cache<boolean>) => any,
|
||||
onSizeChangedCallback: (directionIsRTLCache?: CacheValues<boolean>) => any,
|
||||
options?: SizeObserverOptions
|
||||
): (() => void) => {
|
||||
): SizeObserver => {
|
||||
const { _direction: observeDirectionChange = false, _appear: observeAppearChange = false } = options || {};
|
||||
const rtlScrollBehavior = getEnvironment()._rtlScrollBehavior;
|
||||
const { _rtlScrollBehavior: rtlScrollBehavior } = getEnvironment();
|
||||
const baseElements = createDOM(`<div class="${classNameSizeObserver}"><div class="${classNameSizeObserverListener}"></div></div>`);
|
||||
const sizeObserver = baseElements[0] as HTMLElement;
|
||||
const listenerElement = sizeObserver.firstChild as HTMLElement;
|
||||
const updateResizeObserverContentRectCache = createCache<DOMRectReadOnly, DOMRectReadOnly>(0, {
|
||||
const { _update: updateResizeObserverContentRectCache } = createCache<DOMRectReadOnly, DOMRectReadOnly>(0, {
|
||||
_alwaysUpdateValues: true,
|
||||
_equal: (currVal, newVal) =>
|
||||
!(
|
||||
@@ -74,8 +86,8 @@ export const createSizeObserver = (
|
||||
(!domRectHasDimensions(currVal) && domRectHasDimensions(newVal))
|
||||
),
|
||||
});
|
||||
const onSizeChangedCallbackProxy = (sizeChangedContext?: Cache<boolean> | SizeObserverEntry[] | Event) => {
|
||||
const hasDirectionCache = sizeChangedContext && isBoolean((sizeChangedContext as Cache<boolean>)._value);
|
||||
const onSizeChangedCallbackProxy = (sizeChangedContext?: CacheValues<boolean> | SizeObserverEntry[] | Event) => {
|
||||
const hasDirectionCache = sizeChangedContext && isBoolean((sizeChangedContext as CacheValues<boolean>)._value);
|
||||
|
||||
let skip = false;
|
||||
let doDirectionScroll = true; // always true if sizeChangedContext is Event (appear callback or RO. Polyfill)
|
||||
@@ -88,21 +100,22 @@ export const createSizeObserver = (
|
||||
}
|
||||
// else if its triggered with DirectionCache
|
||||
else if (hasDirectionCache) {
|
||||
doDirectionScroll = (sizeChangedContext as Cache<boolean>)._changed; // direction scroll when DirectionCache changed, false toherwise
|
||||
doDirectionScroll = (sizeChangedContext as CacheValues<boolean>)._changed; // direction scroll when DirectionCache changed, false toherwise
|
||||
}
|
||||
|
||||
if (observeDirectionChange) {
|
||||
const rtl = hasDirectionCache ? (sizeChangedContext as Cache<boolean>)._value : directionIsRTL(sizeObserver);
|
||||
const rtl = hasDirectionCache ? (sizeChangedContext as CacheValues<boolean>)._value : directionIsRTL(sizeObserver);
|
||||
scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount);
|
||||
scrollTop(sizeObserver, scrollAmount);
|
||||
}
|
||||
|
||||
if (!skip) {
|
||||
onSizeChangedCallback(hasDirectionCache ? (sizeChangedContext as Cache<boolean>) : undefined);
|
||||
onSizeChangedCallback(hasDirectionCache ? (sizeChangedContext as CacheValues<boolean>) : undefined);
|
||||
}
|
||||
};
|
||||
const offListeners: (() => void)[] = [];
|
||||
let appearCallback: ((...args: any) => any) | false = observeAppearChange ? onSizeChangedCallbackProxy : false;
|
||||
let directionIsRTLCache: Cache<boolean> | undefined;
|
||||
|
||||
if (ResizeObserverConstructor) {
|
||||
const resizeObserverInstance = new ResizeObserverConstructor(onSizeChangedCallbackProxy);
|
||||
@@ -169,19 +182,20 @@ export const createSizeObserver = (
|
||||
}
|
||||
|
||||
if (observeDirectionChange) {
|
||||
const updateDirectionIsRTLCache = createCache(() => directionIsRTL(sizeObserver));
|
||||
directionIsRTLCache = createCache(() => directionIsRTL(sizeObserver));
|
||||
const { _update: updateDirectionIsRTLCache } = directionIsRTLCache;
|
||||
push(
|
||||
offListeners,
|
||||
on(sizeObserver, scrollEventName, (event: Event) => {
|
||||
const directionIsRTLCache = updateDirectionIsRTLCache();
|
||||
const { _value, _changed } = directionIsRTLCache;
|
||||
const directionIsRTLCacheValues = updateDirectionIsRTLCache();
|
||||
const { _value, _changed } = directionIsRTLCacheValues;
|
||||
if (_changed) {
|
||||
if (_value) {
|
||||
style(listenerElement, { left: 'auto', right: 0 });
|
||||
} else {
|
||||
style(listenerElement, { left: 0, right: 'auto' });
|
||||
}
|
||||
onSizeChangedCallbackProxy(directionIsRTLCache);
|
||||
onSizeChangedCallbackProxy(directionIsRTLCacheValues);
|
||||
}
|
||||
|
||||
preventDefault(event);
|
||||
@@ -205,8 +219,21 @@ export const createSizeObserver = (
|
||||
|
||||
prependChildren(target, sizeObserver);
|
||||
|
||||
return () => {
|
||||
runEach(offListeners);
|
||||
removeElements(sizeObserver);
|
||||
return {
|
||||
_destroy() {
|
||||
runEach(offListeners);
|
||||
removeElements(sizeObserver);
|
||||
},
|
||||
_getCurrentCacheValues(force?: boolean) {
|
||||
return {
|
||||
_directionIsRTL: directionIsRTLCache
|
||||
? directionIsRTLCache._current(force)
|
||||
: {
|
||||
_value: false,
|
||||
_previous: false,
|
||||
_changed: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
WH,
|
||||
Cache,
|
||||
CacheValues,
|
||||
createDOM,
|
||||
offsetSize,
|
||||
runEach,
|
||||
@@ -13,13 +13,25 @@ import {
|
||||
import { createSizeObserver } from 'observers/sizeObserver';
|
||||
import { classNameTrinsicObserver } from 'classnames';
|
||||
|
||||
export interface TrinsicObserver {
|
||||
_destroy(): void;
|
||||
_getCurrentCacheValues(
|
||||
force?: boolean
|
||||
): {
|
||||
_heightIntrinsic: CacheValues<boolean>;
|
||||
};
|
||||
}
|
||||
|
||||
export const createTrinsicObserver = (
|
||||
target: HTMLElement,
|
||||
onTrinsicChangedCallback: (widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>) => any
|
||||
): (() => void) => {
|
||||
onTrinsicChangedCallback: (heightIntrinsic: CacheValues<boolean>) => any
|
||||
): TrinsicObserver => {
|
||||
const trinsicObserver = createDOM(`<div class="${classNameTrinsicObserver}"></div>`)[0] as HTMLElement;
|
||||
const offListeners: (() => void)[] = [];
|
||||
const updateHeightIntrinsicCache = createCache<boolean, IntersectionObserverEntry | WH<number>>(
|
||||
const { _update: updateHeightIntrinsicCache, _current: getCurrentHeightIntrinsicCache } = createCache<
|
||||
boolean,
|
||||
IntersectionObserverEntry | WH<number>
|
||||
>(
|
||||
(ioEntryOrSize: IntersectionObserverEntry | WH<number>) =>
|
||||
(ioEntryOrSize! as WH<number>).h === 0 ||
|
||||
(ioEntryOrSize! as IntersectionObserverEntry).isIntersecting ||
|
||||
@@ -35,10 +47,10 @@ export const createTrinsicObserver = (
|
||||
if (entries && entries.length > 0) {
|
||||
const last = entries.pop();
|
||||
if (last) {
|
||||
const heightIntrinsicCache = updateHeightIntrinsicCache(0, last);
|
||||
const heightIntrinsic = updateHeightIntrinsicCache(0, last);
|
||||
|
||||
if (heightIntrinsicCache._changed) {
|
||||
onTrinsicChangedCallback(false, heightIntrinsicCache);
|
||||
if (heightIntrinsic._changed) {
|
||||
onTrinsicChangedCallback(heightIntrinsic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,16 +67,23 @@ export const createTrinsicObserver = (
|
||||
const heightIntrinsicCache = updateHeightIntrinsicCache(0, newSize);
|
||||
|
||||
if (heightIntrinsicCache._changed) {
|
||||
onTrinsicChangedCallback(false, heightIntrinsicCache);
|
||||
onTrinsicChangedCallback(heightIntrinsicCache);
|
||||
}
|
||||
})
|
||||
})._destroy
|
||||
);
|
||||
}
|
||||
|
||||
prependChildren(target, trinsicObserver);
|
||||
|
||||
return () => {
|
||||
runEach(offListeners);
|
||||
removeElements(trinsicObserver);
|
||||
return {
|
||||
_destroy() {
|
||||
runEach(offListeners);
|
||||
removeElements(trinsicObserver);
|
||||
},
|
||||
_getCurrentCacheValues(force?: boolean) {
|
||||
return {
|
||||
_heightIntrinsic: getCurrentHeightIntrinsicCache(force),
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -25,20 +25,15 @@ export type SizeChangedCallback = (this: any, args?: SizeChangedArgs) => void;
|
||||
export type UpdatedCallback = (this: any, args?: UpdatedArgs) => void;
|
||||
|
||||
export interface Options {
|
||||
className?: string | null;
|
||||
resize?: ResizeBehavior;
|
||||
sizeAutoCapable?: boolean;
|
||||
clipAlways?: boolean;
|
||||
normalizeRTL?: boolean;
|
||||
paddingAbsolute?: boolean;
|
||||
autoUpdate?: boolean | null;
|
||||
autoUpdateInterval?: number;
|
||||
updateOnLoad?: string | ReadonlyArray<string> | null;
|
||||
nativeScrollbarsOverlaid?: {
|
||||
showNativeScrollbars?: boolean;
|
||||
initialize?: boolean;
|
||||
updating?: {
|
||||
elementEvents?: ReadonlyArray<[string, string]> | null;
|
||||
contentMutationDebounce?: number;
|
||||
hostMutationDebounce?: number;
|
||||
resizeDebounce?: number;
|
||||
};
|
||||
overflowBehavior?: {
|
||||
overflow?: {
|
||||
x?: OverflowBehavior;
|
||||
y?: OverflowBehavior;
|
||||
};
|
||||
@@ -46,16 +41,20 @@ export interface Options {
|
||||
visibility?: VisibilityBehavior;
|
||||
autoHide?: AutoHideBehavior;
|
||||
autoHideDelay?: number;
|
||||
dragScrolling?: boolean;
|
||||
clickScrolling?: boolean;
|
||||
touchSupport?: boolean;
|
||||
snapHandle?: boolean;
|
||||
dragScroll?: boolean;
|
||||
clickScroll?: boolean;
|
||||
touch?: boolean;
|
||||
};
|
||||
textarea?: {
|
||||
dynWidth?: boolean;
|
||||
dynHeight?: boolean;
|
||||
inheritedAttrs?: string | ReadonlyArray<string> | null;
|
||||
};
|
||||
nativeScrollbarsOverlaid?: {
|
||||
show?: boolean;
|
||||
initialize?: boolean;
|
||||
};
|
||||
/*
|
||||
callbacks?: {
|
||||
onInitialized?: BasicEventCallback | null;
|
||||
onInitializationWithdrawn?: BasicEventCallback | null;
|
||||
@@ -70,6 +69,7 @@ export interface Options {
|
||||
onHostSizeChanged?: SizeChangedCallback | null;
|
||||
onUpdated?: UpdatedCallback | null;
|
||||
};
|
||||
*/
|
||||
}
|
||||
|
||||
export interface OverflowChangedArgs {
|
||||
|
||||
@@ -8,15 +8,13 @@ import {
|
||||
} from 'support/options';
|
||||
import { ResizeBehavior, OverflowBehavior, VisibilityBehavior, AutoHideBehavior, Options } from 'options';
|
||||
|
||||
const classNameAllowedValues: OptionsTemplateValue<string | null> = [oTypes.string, oTypes.null];
|
||||
const numberAllowedValues: OptionsTemplateValue<number> = oTypes.number;
|
||||
const booleanNullAllowedValues: OptionsTemplateValue<boolean | null> = [oTypes.boolean, oTypes.null];
|
||||
const stringArrayNullAllowedValues: OptionsTemplateValue<string | Array<string> | null> = [oTypes.string, oTypes.array, oTypes.null];
|
||||
const stringArrayNullAllowedValues: OptionsTemplateValue<string | ReadonlyArray<string> | null> = [oTypes.string, oTypes.array, oTypes.null];
|
||||
const booleanTrueTemplate: OptionsWithOptionsTemplateValue<boolean> = [true, oTypes.boolean];
|
||||
const booleanFalseTemplate: OptionsWithOptionsTemplateValue<boolean> = [false, oTypes.boolean];
|
||||
const callbackTemplate: OptionsWithOptionsTemplateValue<Func | null> = [null, [oTypes.function, oTypes.null]];
|
||||
// const callbackTemplate: OptionsWithOptionsTemplateValue<Func | null> = [null, [oTypes.function, oTypes.null]];
|
||||
const resizeAllowedValues: OptionsTemplateValue<ResizeBehavior> = 'none both horizontal vertical';
|
||||
const overflowBehaviorAllowedValues: OptionsTemplateValue<OverflowBehavior> = 'visible-hidden visible-scroll scroll hidden';
|
||||
const overflowAllowedValues: OptionsTemplateValue<OverflowBehavior> = 'visible-hidden visible-scroll scroll hidden';
|
||||
const scrollbarsVisibilityAllowedValues: OptionsTemplateValue<VisibilityBehavior> = 'visible hidden auto';
|
||||
const scrollbarsAutoHideAllowedValues: OptionsTemplateValue<AutoHideBehavior> = 'never scroll leavemove';
|
||||
|
||||
@@ -36,37 +34,36 @@ const scrollbarsAutoHideAllowedValues: OptionsTemplateValue<AutoHideBehavior> =
|
||||
* Property "b" has a default value of 250 and it can be number
|
||||
*/
|
||||
const defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<Options>> = {
|
||||
className: ['os-theme-dark', classNameAllowedValues], // null || string
|
||||
resize: ['none', resizeAllowedValues], // none || both || horizontal || vertical || n || b || h || v
|
||||
sizeAutoCapable: booleanTrueTemplate, // true || false
|
||||
clipAlways: booleanTrueTemplate, // true || false
|
||||
normalizeRTL: booleanTrueTemplate, // true || false
|
||||
paddingAbsolute: booleanFalseTemplate, // true || false
|
||||
autoUpdate: [null, booleanNullAllowedValues], // true || false || null
|
||||
autoUpdateInterval: [33, numberAllowedValues], // number
|
||||
updateOnLoad: [['img'], stringArrayNullAllowedValues], // string || array || null
|
||||
nativeScrollbarsOverlaid: {
|
||||
showNativeScrollbars: booleanFalseTemplate, // true || false
|
||||
initialize: booleanFalseTemplate, // true || false
|
||||
updating: {
|
||||
elementEvents: [[['img', 'load']], [oTypes.array, oTypes.null]], // array of tuples || null
|
||||
contentMutationDebounce: [80, numberAllowedValues], // number
|
||||
hostMutationDebounce: [0, numberAllowedValues], // number
|
||||
resizeDebounce: [0, numberAllowedValues], // number
|
||||
},
|
||||
overflowBehavior: {
|
||||
x: ['scroll', overflowBehaviorAllowedValues], // visible-hidden || visible-scroll || hidden || scroll || v-h || v-s || h || s
|
||||
y: ['scroll', overflowBehaviorAllowedValues], // visible-hidden || visible-scroll || hidden || scroll || v-h || v-s || h || s
|
||||
overflow: {
|
||||
x: ['scroll', overflowAllowedValues], // visible-hidden || visible-scroll || hidden || scroll || v-h || v-s || h || s
|
||||
y: ['scroll', overflowAllowedValues], // visible-hidden || visible-scroll || hidden || scroll || v-h || v-s || h || s
|
||||
},
|
||||
scrollbars: {
|
||||
visibility: ['auto', scrollbarsVisibilityAllowedValues], // visible || hidden || auto || v || h || a
|
||||
autoHide: ['never', scrollbarsAutoHideAllowedValues], // never || scroll || leave || move || n || s || l || m
|
||||
autoHideDelay: [800, numberAllowedValues], // number
|
||||
dragScrolling: booleanTrueTemplate, // true || false
|
||||
clickScrolling: booleanFalseTemplate, // true || false
|
||||
touchSupport: booleanTrueTemplate, // true || false
|
||||
snapHandle: booleanFalseTemplate, // true || false
|
||||
dragScroll: booleanTrueTemplate, // true || false
|
||||
clickScroll: booleanFalseTemplate, // true || false
|
||||
touch: booleanTrueTemplate, // true || false
|
||||
},
|
||||
textarea: {
|
||||
dynWidth: booleanFalseTemplate, // true || false
|
||||
dynHeight: booleanFalseTemplate, // true || false
|
||||
inheritedAttrs: [['style', 'class'], stringArrayNullAllowedValues], // string || array || null
|
||||
},
|
||||
nativeScrollbarsOverlaid: {
|
||||
show: booleanFalseTemplate, // true || false
|
||||
initialize: booleanFalseTemplate, // true || false
|
||||
},
|
||||
/*
|
||||
callbacks: {
|
||||
onInitialized: callbackTemplate, // null || function
|
||||
onInitializationWithdrawn: callbackTemplate, // null || function
|
||||
@@ -81,6 +78,7 @@ const defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<Options>>
|
||||
onHostSizeChanged: callbackTemplate, // null || function
|
||||
onUpdated: callbackTemplate, // null || function
|
||||
},
|
||||
*/
|
||||
};
|
||||
|
||||
export const { _template: optionsTemplate, _options: defaultOptions } = transformOptions(defaultOptionsWithTemplate);
|
||||
|
||||
@@ -51,10 +51,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
#os-environment
|
||||
/* fix restricted measuring */
|
||||
#os-environment:before,
|
||||
#os-environment:after,
|
||||
.os-environment:before,
|
||||
.os-environment:after,
|
||||
.os-content:before,
|
||||
.os-content:after {
|
||||
content: '';
|
||||
@@ -67,17 +66,17 @@
|
||||
flex-shrink: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
#os-environment,
|
||||
.os-environment,
|
||||
.os-viewport {
|
||||
-ms-overflow-style: scrollbar !important;
|
||||
}
|
||||
.os-viewport-scrollbar-styled#os-environment,
|
||||
.os-viewport-scrollbar-styled.os-environment,
|
||||
.os-viewport-scrollbar-styled.os-viewport {
|
||||
scrollbar-width: none !important;
|
||||
}
|
||||
.os-viewport-scrollbar-styled#os-environment::-webkit-scrollbar,
|
||||
.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-environment::-webkit-scrollbar-corner,
|
||||
.os-viewport-scrollbar-styled.os-viewport::-webkit-scrollbar-corner {
|
||||
display: none !important;
|
||||
width: 0px !important;
|
||||
@@ -85,3 +84,9 @@
|
||||
visibility: hidden !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.os-content-arrange {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -1,49 +1,38 @@
|
||||
import { OSTarget, OSTargetObject } from 'typings';
|
||||
import { createStructureLifecycle } from 'lifecycles/structureLifecycle';
|
||||
import { Cache, each, push } from 'support';
|
||||
import { createSizeObserver } from 'observers/sizeObserver';
|
||||
import { createTrinsicObserver } from 'observers/trinsicObserver';
|
||||
import { createDOMObserver } from 'observers/domObserver';
|
||||
|
||||
import { validateOptions, assignDeep, isEmptyObject } from 'support';
|
||||
import { createStructureSetup, StructureSetup } from 'setups/structureSetup';
|
||||
import { Lifecycle } from 'lifecycles/lifecycleBase';
|
||||
import { createLifecycleHub } from 'lifecycles/lifecycleHub';
|
||||
import { Options, defaultOptions, optionsTemplate } from 'options';
|
||||
|
||||
const OverlayScrollbars = (target: OSTarget | OSTargetObject, options?: any, extensions?: any): void => {
|
||||
const structureSetup: StructureSetup = createStructureSetup(target);
|
||||
const lifecycles: Lifecycle<any>[] = [];
|
||||
const { _host, _viewport, _content } = structureSetup._targetObj;
|
||||
|
||||
push(lifecycles, createStructureLifecycle(structureSetup._targetObj));
|
||||
|
||||
// eslint-disable-next-line
|
||||
const onSizeChanged = (directionCache?: Cache<boolean>) => {
|
||||
if (directionCache) {
|
||||
each(lifecycles, (lifecycle) => {
|
||||
lifecycle._onDirectionChanged && lifecycle._onDirectionChanged(directionCache);
|
||||
});
|
||||
} else {
|
||||
each(lifecycles, (lifecycle) => {
|
||||
lifecycle._onSizeChanged && lifecycle._onSizeChanged();
|
||||
});
|
||||
}
|
||||
};
|
||||
const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>) => {
|
||||
each(lifecycles, (lifecycle) => {
|
||||
lifecycle._onTrinsicChanged && lifecycle._onTrinsicChanged(widthIntrinsic, heightIntrinsicCache);
|
||||
});
|
||||
};
|
||||
|
||||
createSizeObserver(_host, onSizeChanged, { _appear: true, _direction: true });
|
||||
createTrinsicObserver(_host, onTrinsicChanged);
|
||||
createDOMObserver(_host, () => {
|
||||
return null;
|
||||
});
|
||||
createDOMObserver(
|
||||
_content || _viewport,
|
||||
() => {
|
||||
return null;
|
||||
},
|
||||
{ _observeContent: true }
|
||||
const OverlayScrollbars = (target: OSTarget | OSTargetObject, options?: Options, extensions?: any): any => {
|
||||
const currentOptions: Required<Options> = assignDeep(
|
||||
{},
|
||||
defaultOptions,
|
||||
validateOptions<Options>(options || ({} as Options), optionsTemplate, null, true)._validated
|
||||
);
|
||||
const structureSetup: StructureSetup = createStructureSetup(target);
|
||||
const lifecycleHub = createLifecycleHub(currentOptions, structureSetup);
|
||||
const instance = {
|
||||
options(newOptions?: Options) {
|
||||
if (newOptions) {
|
||||
const { _validated: _changedOptions } = validateOptions(newOptions, optionsTemplate, currentOptions, true);
|
||||
|
||||
if (!isEmptyObject(_changedOptions)) {
|
||||
assignDeep(currentOptions, _changedOptions);
|
||||
lifecycleHub._update(_changedOptions);
|
||||
}
|
||||
}
|
||||
return currentOptions;
|
||||
},
|
||||
update(force?: boolean) {
|
||||
lifecycleHub._update(null, force);
|
||||
},
|
||||
};
|
||||
|
||||
instance.update(true);
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
export { OverlayScrollbars };
|
||||
|
||||
@@ -12,8 +12,17 @@ import {
|
||||
removeClass,
|
||||
push,
|
||||
runEach,
|
||||
prependChildren,
|
||||
} from 'support';
|
||||
import { classNameHost, classNamePadding, classNameViewport, classNameContent } from 'classnames';
|
||||
import {
|
||||
classNameHost,
|
||||
classNamePadding,
|
||||
classNameViewport,
|
||||
classNameContent,
|
||||
classNameContentArrange,
|
||||
classNameViewportScrollbarStyling,
|
||||
} from 'classnames';
|
||||
import { getEnvironment } from 'environment';
|
||||
import { OSTarget, OSTargetObject, InternalVersionOf, OSTargetElement } from 'typings';
|
||||
|
||||
export interface OSTargetContext {
|
||||
@@ -27,6 +36,7 @@ export interface OSTargetContext {
|
||||
|
||||
export interface PreparedOSTargetObject extends Required<InternalVersionOf<OSTargetObject>> {
|
||||
_host: HTMLElement;
|
||||
_contentArrange: HTMLElement | null;
|
||||
}
|
||||
|
||||
export interface StructureSetup {
|
||||
@@ -150,6 +160,25 @@ export const createStructureSetup = (target: OSTarget | OSTargetObject): Structu
|
||||
_host,
|
||||
};
|
||||
|
||||
const { _nativeScrollbarStyling, _nativeScrollbarIsOverlaid } = getEnvironment();
|
||||
if (_nativeScrollbarStyling) {
|
||||
addClass(_viewport, classNameViewportScrollbarStyling);
|
||||
push(destroyFns, () => {
|
||||
removeClass(_viewport, classNameViewportScrollbarStyling);
|
||||
});
|
||||
} else if (_nativeScrollbarIsOverlaid.x || _nativeScrollbarIsOverlaid.y) {
|
||||
if (obj._content) {
|
||||
const contentArrangeElm = createDiv(classNameContentArrange);
|
||||
|
||||
prependChildren(_viewport, contentArrangeElm);
|
||||
push(destroyFns, () => {
|
||||
removeElements(contentArrangeElm);
|
||||
});
|
||||
|
||||
obj._contentArrange = contentArrangeElm;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
_targetObj: obj,
|
||||
_targetCtx: ctx,
|
||||
|
||||
+19
-5
@@ -1,7 +1,7 @@
|
||||
export interface Cache<T> {
|
||||
export interface CacheValues<T> {
|
||||
readonly _value?: T;
|
||||
readonly _previous?: T;
|
||||
readonly _changed: boolean;
|
||||
_changed: boolean;
|
||||
}
|
||||
|
||||
export interface CacheOptions<T> {
|
||||
@@ -13,7 +13,14 @@ export interface CacheOptions<T> {
|
||||
_alwaysUpdateValues?: boolean;
|
||||
}
|
||||
|
||||
export type CacheUpdate<T, C> = undefined extends C ? (force?: boolean | 0, context?: C) => Cache<T> : (force: boolean | 0, context: C) => Cache<T>;
|
||||
export interface Cache<T, C = undefined> {
|
||||
_current: (force?: boolean) => CacheValues<T>;
|
||||
_update: CacheUpdate<T, C>;
|
||||
}
|
||||
|
||||
export type CacheUpdate<T, C> = undefined extends C
|
||||
? (force?: boolean | 0, context?: C) => CacheValues<T>
|
||||
: (force: boolean | 0, context: C) => CacheValues<T>;
|
||||
|
||||
export type UpdateCachePropFunction<T, C> = undefined extends C
|
||||
? (context?: C, current?: T, previous?: T) => T
|
||||
@@ -23,7 +30,7 @@ export type UpdateCachePropFunction<T, C> = undefined extends C
|
||||
|
||||
export type EqualCachePropFunction<T> = (currentVal?: T, newVal?: T) => boolean;
|
||||
|
||||
export const createCache = <T, C = undefined>(update: UpdateCachePropFunction<T, C>, options?: CacheOptions<T>): CacheUpdate<T, C> => {
|
||||
export const createCache = <T, C = undefined>(update: UpdateCachePropFunction<T, C>, options?: CacheOptions<T>): Cache<T, C> => {
|
||||
const { _equal, _initialValue, _alwaysUpdateValues } = options || {};
|
||||
let _value: T | undefined = _initialValue;
|
||||
let _previous: T | undefined;
|
||||
@@ -48,5 +55,12 @@ export const createCache = <T, C = undefined>(update: UpdateCachePropFunction<T,
|
||||
};
|
||||
}) as CacheUpdate<T, C>;
|
||||
|
||||
return cacheUpdate;
|
||||
return {
|
||||
_update: cacheUpdate,
|
||||
_current: (force?: boolean) => ({
|
||||
_value,
|
||||
_previous,
|
||||
_changed: !!force,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -37,9 +37,9 @@ export interface OnOptions {
|
||||
* @param listener The listener which shall be removed.
|
||||
* @param capture The options of the removed listener.
|
||||
*/
|
||||
export const off = (target: EventTarget, eventNames: string, listener: EventListener, capture?: boolean): void => {
|
||||
export const off = <T extends Event = Event>(target: EventTarget, eventNames: string, listener: (event: T) => any, capture?: boolean): void => {
|
||||
each(splitEventNames(eventNames), (eventName) => {
|
||||
target.removeEventListener(eventName, listener, capture);
|
||||
target.removeEventListener(eventName, listener as EventListener, capture);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -50,7 +50,12 @@ export const off = (target: EventTarget, eventNames: string, listener: EventList
|
||||
* @param listener The listener which is called on the eventnames.
|
||||
* @param options The options of the added listener.
|
||||
*/
|
||||
export const on = (target: EventTarget, eventNames: string, listener: EventListener, options?: OnOptions): (() => void) => {
|
||||
export const on = <T extends Event = Event>(
|
||||
target: EventTarget,
|
||||
eventNames: string,
|
||||
listener: (event: T) => any,
|
||||
options?: OnOptions
|
||||
): (() => void) => {
|
||||
const doSupportPassiveEvents = supportPassiveEvents();
|
||||
const passive = (doSupportPassiveEvents && options && options._passive) || false;
|
||||
const capture = (options && options._capture) || false;
|
||||
@@ -64,12 +69,12 @@ export const on = (target: EventTarget, eventNames: string, listener: EventListe
|
||||
: capture;
|
||||
|
||||
each(splitEventNames(eventNames), (eventName) => {
|
||||
const finalListener = once
|
||||
? (evt: Event) => {
|
||||
const finalListener = (once
|
||||
? (evt: T) => {
|
||||
target.removeEventListener(eventName, finalListener, capture);
|
||||
listener && listener(evt);
|
||||
}
|
||||
: listener;
|
||||
: listener) as EventListener;
|
||||
|
||||
push(offListeners, off.bind(null, target, eventName, finalListener, capture));
|
||||
target.addEventListener(eventName, finalListener, nativeOptions);
|
||||
|
||||
@@ -39,7 +39,7 @@ type OptionsTemplateTypeMap = {
|
||||
__TPL_boolean_TYPE__: boolean;
|
||||
__TPL_number_TYPE__: number;
|
||||
__TPL_string_TYPE__: string;
|
||||
__TPL_array_TYPE__: Array<any>;
|
||||
__TPL_array_TYPE__: Array<any> | ReadonlyArray<any>;
|
||||
__TPL_function_TYPE__: Func;
|
||||
__TPL_null_TYPE__: null;
|
||||
__TPL_object_TYPE__: Record<string, unknown>;
|
||||
|
||||
+62
-1
@@ -1,6 +1,67 @@
|
||||
import 'overlayscrollbars.scss';
|
||||
import './index.scss';
|
||||
import { createDiv, appendChildren, parent, style, on, off, addClass, WH, XY, clientSize } from 'support';
|
||||
import { OverlayScrollbars } from 'overlayscrollbars/OverlayScrollbars';
|
||||
|
||||
const targetElm = document.querySelector('#target') as HTMLElement;
|
||||
OverlayScrollbars(targetElm);
|
||||
window.os = OverlayScrollbars(targetElm);
|
||||
|
||||
export const resize = (element: HTMLElement) => {
|
||||
const dragStartSize: WH<number> = { w: 0, h: 0 };
|
||||
const dragStartPosition: XY<number> = { x: 0, y: 0 };
|
||||
const resizeBtn = createDiv('resizeBtn');
|
||||
appendChildren(element, resizeBtn);
|
||||
addClass(element, 'resizer');
|
||||
|
||||
let dragResizeBtn: HTMLElement | undefined;
|
||||
let dragResizer: HTMLElement | undefined;
|
||||
|
||||
const onSelectStart = (event: Event) => {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
};
|
||||
|
||||
const resizerResize = (event: MouseEvent) => {
|
||||
const sizeStyle = {
|
||||
width: dragStartSize.w + event.pageX - dragStartPosition.x,
|
||||
height: dragStartSize.h + event.pageY - dragStartPosition.y,
|
||||
};
|
||||
|
||||
style(dragResizer, sizeStyle);
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const resizerResized = (event: MouseEvent) => {
|
||||
off(document, 'selectstart', onSelectStart);
|
||||
off(document, 'mousemove', resizerResize);
|
||||
off(document, 'mouseup', resizerResized);
|
||||
|
||||
dragResizer = undefined;
|
||||
dragResizeBtn = undefined;
|
||||
};
|
||||
|
||||
on(resizeBtn, 'mousedown', (event: MouseEvent) => {
|
||||
const { currentTarget } = event;
|
||||
if (event.buttons === 1 || event.which === 1) {
|
||||
dragStartPosition.x = event.pageX;
|
||||
dragStartPosition.y = event.pageY;
|
||||
|
||||
dragResizeBtn = currentTarget as HTMLElement;
|
||||
dragResizer = parent(currentTarget as HTMLElement) as HTMLElement;
|
||||
|
||||
const cSize = clientSize(element);
|
||||
dragStartSize.w = cSize.w;
|
||||
dragStartSize.h = cSize.h;
|
||||
|
||||
on(document, 'selectstart', onSelectStart);
|
||||
on(document, 'mousemove', resizerResize);
|
||||
on(document, 'mouseup', resizerResized);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
resize(document.querySelector('#resize')!);
|
||||
resize(document.querySelector('#target')!);
|
||||
|
||||
@@ -29,7 +29,6 @@ body {
|
||||
|
||||
#target {
|
||||
overflow: hidden;
|
||||
resize: both;
|
||||
position: relative;
|
||||
border: 2px solid red;
|
||||
min-height: 100px;
|
||||
@@ -40,7 +39,6 @@ body {
|
||||
|
||||
#resize {
|
||||
overflow: hidden;
|
||||
resize: both;
|
||||
background: blue;
|
||||
border: 1px solid black;
|
||||
padding: 10px;
|
||||
@@ -135,3 +133,18 @@ body {
|
||||
.directionRTL {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.resizer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.resizeBtn {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background: red;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@@ -15,15 +15,17 @@ const createUpdater = <T, C = unknown>(updaterReturn: (i: number) => T) => {
|
||||
describe('cache', () => {
|
||||
test('creates and updates cache', () => {
|
||||
const [fn, updater] = createUpdater((i) => `${i}`);
|
||||
const update = createCache<string>(updater);
|
||||
const { _update, _current } = createCache<string>(updater);
|
||||
|
||||
let { _value, _previous, _changed } = update();
|
||||
let { _value, _previous, _changed } = _update();
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
|
||||
expect(_value).toBe('1');
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update());
|
||||
({ _value, _previous, _changed } = _update());
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, '1', undefined);
|
||||
expect(_value).toBe('2');
|
||||
expect(_previous).toBe('1');
|
||||
@@ -41,16 +43,19 @@ describe('cache', () => {
|
||||
updateFn(context, current, previous);
|
||||
return context!.test === 'test' || context!.even % 2 === 0;
|
||||
};
|
||||
const update = createCache(updater);
|
||||
const { _update, _current } = createCache(updater);
|
||||
const firstCtx = { test: 'test', even: 2 };
|
||||
|
||||
let { _value, _previous, _changed } = update(0, firstCtx);
|
||||
let { _value, _previous, _changed } = _update(0, firstCtx);
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(updateFn).toHaveBeenLastCalledWith(firstCtx, undefined, undefined);
|
||||
expect(_value).toBe(true);
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(true);
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
|
||||
({ _value, _previous, _changed } = update(0, firstCtx));
|
||||
({ _value, _previous, _changed } = _update(0, firstCtx));
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(updateFn).toHaveBeenLastCalledWith(firstCtx, true, undefined);
|
||||
expect(_value).toBe(true);
|
||||
expect(_previous).toBe(undefined);
|
||||
@@ -58,19 +63,22 @@ describe('cache', () => {
|
||||
|
||||
const scndCtx = { test: 'nah', even: 1 };
|
||||
|
||||
({ _value, _previous, _changed } = update(0, scndCtx));
|
||||
({ _value, _previous, _changed } = _update(0, scndCtx));
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(updateFn).toHaveBeenLastCalledWith(scndCtx, true, undefined);
|
||||
expect(_value).toBe(false);
|
||||
expect(_previous).toBe(true);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update(0, scndCtx));
|
||||
({ _value, _previous, _changed } = _update(0, scndCtx));
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(updateFn).toHaveBeenLastCalledWith(scndCtx, false, true);
|
||||
expect(_value).toBe(false);
|
||||
expect(_previous).toBe(true);
|
||||
expect(_changed).toBe(false);
|
||||
|
||||
({ _value, _previous, _changed } = update(true, scndCtx));
|
||||
({ _value, _previous, _changed } = _update(true, scndCtx));
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(updateFn).toHaveBeenLastCalledWith(scndCtx, false, true);
|
||||
expect(_value).toBe(false);
|
||||
expect(_previous).toBe(false);
|
||||
@@ -82,32 +90,32 @@ describe('cache', () => {
|
||||
test: string;
|
||||
even: number;
|
||||
}
|
||||
const update = createCache<ContextObj, ContextObj>(0);
|
||||
const { _update } = createCache<ContextObj, ContextObj>(0);
|
||||
const firstCtx = { test: 'test', even: 2 };
|
||||
|
||||
let { _value, _previous, _changed } = update(0, firstCtx);
|
||||
let { _value, _previous, _changed } = _update(0, firstCtx);
|
||||
expect(_value).toBe(firstCtx);
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update(0, firstCtx));
|
||||
({ _value, _previous, _changed } = _update(0, firstCtx));
|
||||
expect(_value).toBe(firstCtx);
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(false);
|
||||
|
||||
const scndCtx = { test: 'nah', even: 1 };
|
||||
|
||||
({ _value, _previous, _changed } = update(0, scndCtx));
|
||||
({ _value, _previous, _changed } = _update(0, scndCtx));
|
||||
expect(_value).toBe(scndCtx);
|
||||
expect(_previous).toBe(firstCtx);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update(0, scndCtx));
|
||||
({ _value, _previous, _changed } = _update(0, scndCtx));
|
||||
expect(_value).toBe(scndCtx);
|
||||
expect(_previous).toBe(firstCtx);
|
||||
expect(_changed).toBe(false);
|
||||
|
||||
({ _value, _previous, _changed } = update(true, scndCtx));
|
||||
({ _value, _previous, _changed } = _update(true, scndCtx));
|
||||
expect(_value).toBe(scndCtx);
|
||||
expect(_previous).toBe(scndCtx);
|
||||
expect(_changed).toBe(true);
|
||||
@@ -117,15 +125,17 @@ describe('cache', () => {
|
||||
describe('equal', () => {
|
||||
test('with equal always true', () => {
|
||||
const [fn, updater] = createUpdater((i) => i);
|
||||
const update = createCache<number>(updater, { _equal: () => true });
|
||||
const { _update, _current } = createCache<number>(updater, { _equal: () => true });
|
||||
|
||||
let { _value, _previous, _changed } = update();
|
||||
let { _value, _previous, _changed } = _update();
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
|
||||
expect(_value).toBe(undefined);
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(false);
|
||||
|
||||
({ _value, _previous, _changed } = update());
|
||||
({ _value, _previous, _changed } = _update());
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
|
||||
expect(_value).toBe(undefined);
|
||||
expect(_previous).toBe(undefined);
|
||||
@@ -134,15 +144,17 @@ describe('cache', () => {
|
||||
|
||||
test('with equal always false', () => {
|
||||
const [fn, updater] = createUpdater(() => 1);
|
||||
const update = createCache<number>(updater, { _equal: () => false });
|
||||
const { _update, _current } = createCache<number>(updater, { _equal: () => false });
|
||||
|
||||
let { _value, _previous, _changed } = update();
|
||||
let { _value, _previous, _changed } = _update();
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
|
||||
expect(_value).toBe(1);
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update());
|
||||
({ _value, _previous, _changed } = _update());
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, 1, undefined);
|
||||
expect(_value).toBe(1);
|
||||
expect(_previous).toBe(1);
|
||||
@@ -152,15 +164,15 @@ describe('cache', () => {
|
||||
test('with object equal', () => {
|
||||
const obj = { a: -1, b: -1 };
|
||||
const [fn, updater] = createUpdater((i) => ({ a: i, b: i + 1 }));
|
||||
const update = createCache<typeof obj>(updater, { _equal: (a, b) => a?.a === b?.a && a?.b === b?.b });
|
||||
const { _update } = createCache<typeof obj>(updater, { _equal: (a, b) => a?.a === b?.a && a?.b === b?.b });
|
||||
|
||||
let { _value, _previous, _changed } = update();
|
||||
let { _value, _previous, _changed } = _update();
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
|
||||
expect(_value).toEqual({ a: 1, b: 2 });
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update());
|
||||
({ _value, _previous, _changed } = _update());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, { a: 1, b: 2 }, undefined);
|
||||
expect(_value).toEqual({ a: 2, b: 3 });
|
||||
expect(_previous).toEqual({ a: 1, b: 2 });
|
||||
@@ -171,15 +183,17 @@ describe('cache', () => {
|
||||
describe('inital value', () => {
|
||||
test('creates and updates cache with initialValue', () => {
|
||||
const [fn, updater] = createUpdater((i) => i);
|
||||
const update = createCache<number>(updater, { _initialValue: 0 });
|
||||
const { _update, _current } = createCache<number>(updater, { _initialValue: 0 });
|
||||
|
||||
let { _value, _previous, _changed } = update();
|
||||
let { _value, _previous, _changed } = _update();
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, 0, undefined);
|
||||
expect(_value).toBe(1);
|
||||
expect(_previous).toBe(0);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update());
|
||||
({ _value, _previous, _changed } = _update());
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, 1, 0);
|
||||
expect(_value).toBe(2);
|
||||
expect(_previous).toBe(1);
|
||||
@@ -189,15 +203,15 @@ describe('cache', () => {
|
||||
test('creates and updates cache with initialValue and equal', () => {
|
||||
const obj = { a: -1, b: -1 };
|
||||
const [fn, updater] = createUpdater((i) => ({ a: i, b: i + 1 }));
|
||||
const update = createCache<typeof obj>(updater, { _initialValue: obj, _equal: (a, b) => a?.a === b?.a && a?.b === b?.b });
|
||||
const { _update } = createCache<typeof obj>(updater, { _initialValue: obj, _equal: (a, b) => a?.a === b?.a && a?.b === b?.b });
|
||||
|
||||
let { _value, _previous, _changed } = update();
|
||||
let { _value, _previous, _changed } = _update();
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, obj, undefined);
|
||||
expect(_value).toEqual({ a: 1, b: 2 });
|
||||
expect(_previous).toBe(obj);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update());
|
||||
({ _value, _previous, _changed } = _update());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, { a: 1, b: 2 }, obj);
|
||||
expect(_value).toEqual({ a: 2, b: 3 });
|
||||
expect(_previous).toEqual({ a: 1, b: 2 });
|
||||
@@ -208,15 +222,15 @@ describe('cache', () => {
|
||||
describe('always update values', () => {
|
||||
test('creates and updates cache with alwaysUpdateValues and equal always true', () => {
|
||||
const [fn, updater] = createUpdater((i) => i);
|
||||
const update = createCache<number>(updater, { _alwaysUpdateValues: true, _equal: () => true });
|
||||
const { _update } = createCache<number>(updater, { _alwaysUpdateValues: true, _equal: () => true });
|
||||
|
||||
let { _value, _previous, _changed } = update();
|
||||
let { _value, _previous, _changed } = _update();
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
|
||||
expect(_value).toBe(1);
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(false);
|
||||
|
||||
({ _value, _previous, _changed } = update());
|
||||
({ _value, _previous, _changed } = _update());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, 1, undefined);
|
||||
expect(_value).toBe(2);
|
||||
expect(_previous).toBe(1);
|
||||
@@ -228,32 +242,37 @@ describe('cache', () => {
|
||||
test: string;
|
||||
even: number;
|
||||
}
|
||||
const update = createCache<ContextObj, ContextObj>(0, { _alwaysUpdateValues: true });
|
||||
const { _update, _current } = createCache<ContextObj, ContextObj>(0, { _alwaysUpdateValues: true });
|
||||
const firstCtx = { test: 'test', even: 2 };
|
||||
|
||||
let { _value, _previous, _changed } = update(0, firstCtx);
|
||||
let { _value, _previous, _changed } = _update(0, firstCtx);
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(_value).toBe(firstCtx);
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update(0, firstCtx));
|
||||
({ _value, _previous, _changed } = _update(0, firstCtx));
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(_value).toBe(firstCtx);
|
||||
expect(_previous).toBe(firstCtx);
|
||||
expect(_changed).toBe(false);
|
||||
|
||||
const scndCtx = { test: 'nah', even: 1 };
|
||||
|
||||
({ _value, _previous, _changed } = update(0, scndCtx));
|
||||
({ _value, _previous, _changed } = _update(0, scndCtx));
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(_value).toBe(scndCtx);
|
||||
expect(_previous).toBe(firstCtx);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update(0, scndCtx));
|
||||
({ _value, _previous, _changed } = _update(0, scndCtx));
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(_value).toBe(scndCtx);
|
||||
expect(_previous).toBe(scndCtx);
|
||||
expect(_changed).toBe(false);
|
||||
|
||||
({ _value, _previous, _changed } = update(true, scndCtx));
|
||||
({ _value, _previous, _changed } = _update(true, scndCtx));
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(_value).toBe(scndCtx);
|
||||
expect(_previous).toBe(scndCtx);
|
||||
expect(_changed).toBe(true);
|
||||
@@ -263,15 +282,17 @@ describe('cache', () => {
|
||||
describe('constant', () => {
|
||||
test('updates constant initially without intial value', () => {
|
||||
const [fn, updater] = createUpdater(() => true);
|
||||
const update = createCache<boolean>(updater);
|
||||
const { _update, _current } = createCache<boolean>(updater);
|
||||
|
||||
let { _value, _previous, _changed } = update();
|
||||
let { _value, _previous, _changed } = _update();
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
|
||||
expect(_value).toBe(true);
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update());
|
||||
({ _value, _previous, _changed } = _update());
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, true, undefined);
|
||||
expect(_value).toBe(true);
|
||||
expect(_previous).toBe(undefined);
|
||||
@@ -281,15 +302,15 @@ describe('cache', () => {
|
||||
test('doesnt update constant with initial value', () => {
|
||||
const obj = { constant: true };
|
||||
const [fn, updater] = createUpdater(() => obj);
|
||||
const update = createCache<typeof obj>(updater, { _initialValue: obj });
|
||||
const { _update } = createCache<typeof obj>(updater, { _initialValue: obj });
|
||||
|
||||
let { _value, _previous, _changed } = update();
|
||||
let { _value, _previous, _changed } = _update();
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, obj, undefined);
|
||||
expect(_value).toBe(obj);
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(false);
|
||||
|
||||
({ _value, _previous, _changed } = update());
|
||||
({ _value, _previous, _changed } = _update());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, obj, undefined);
|
||||
expect(_value).toBe(obj);
|
||||
expect(_previous).toBe(undefined);
|
||||
@@ -298,27 +319,31 @@ describe('cache', () => {
|
||||
|
||||
test('updates constant with force', () => {
|
||||
const [fn, updater] = createUpdater(() => 'constant');
|
||||
const update = createCache<string>(updater);
|
||||
const { _update, _current } = createCache<string>(updater);
|
||||
|
||||
let { _value, _previous, _changed } = update();
|
||||
let { _value, _previous, _changed } = _update();
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
|
||||
expect(_value).toBe('constant');
|
||||
expect(_previous).toBe(undefined);
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update(true));
|
||||
({ _value, _previous, _changed } = _update(true));
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, 'constant', undefined);
|
||||
expect(_value).toBe('constant');
|
||||
expect(_previous).toBe('constant');
|
||||
expect(_changed).toBe(true);
|
||||
|
||||
({ _value, _previous, _changed } = update(false));
|
||||
({ _value, _previous, _changed } = _update(false));
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, 'constant', 'constant');
|
||||
expect(_value).toBe('constant');
|
||||
expect(_previous).toBe('constant');
|
||||
expect(_changed).toBe(false);
|
||||
|
||||
({ _value, _previous, _changed } = update());
|
||||
({ _value, _previous, _changed } = _update());
|
||||
expect({ _value, _previous, _changed: false }).toEqual(_current());
|
||||
expect(fn).toHaveBeenLastCalledWith(undefined, 'constant', 'constant');
|
||||
expect(_value).toBe('constant');
|
||||
expect(_previous).toBe('constant');
|
||||
|
||||
Reference in New Issue
Block a user