overflow lifecycle

This commit is contained in:
Rene
2021-03-28 17:34:23 +02:00
parent d6da015835
commit 37893216b7
22 changed files with 771 additions and 314 deletions
@@ -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),
};
},
};
};
+15 -15
View File
@@ -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
View File
@@ -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>;
@@ -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');