diff --git a/packages/overlayscrollbars/src/autoUpdateLoop/autoUpdateLoop.ts b/packages/overlayscrollbars/src/autoUpdateLoop/autoUpdateLoop.ts index 1efa2be..b061d27 100644 --- a/packages/overlayscrollbars/src/autoUpdateLoop/autoUpdateLoop.ts +++ b/packages/overlayscrollbars/src/autoUpdateLoop/autoUpdateLoop.ts @@ -19,7 +19,7 @@ const createAutoUpdateLoop = (): AutoUpdateLoop => { const updateLoopInterval = () => { loopInterval = isEmptyArray(intervals) ? defaultLoopInterval : Math.min.apply(null, intervals); }; - const updateTimeCache = createCache((ctx) => ctx || performance.now(), { + const { _update: updateTimeCache } = createCache((ctx) => ctx || performance.now(), { _initialValue: performance.now(), _equal: (currTime, newTime) => { const delta = newTime! - currTime!; diff --git a/packages/overlayscrollbars/src/classnames.ts b/packages/overlayscrollbars/src/classnames.ts index a196984..958e111 100644 --- a/packages/overlayscrollbars/src/classnames.ts +++ b/packages/overlayscrollbars/src/classnames.ts @@ -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'; diff --git a/packages/overlayscrollbars/src/environment/environment.ts b/packages/overlayscrollbars/src/environment/environment.ts index 89ca6a6..242eae4 100644 --- a/packages/overlayscrollbars/src/environment/environment.ts +++ b/packages/overlayscrollbars/src/environment/environment.ts @@ -122,17 +122,18 @@ const createEnvironment = (): Environment => { const envChildElm = envElm.firstChild as HTMLElement; const onChangedListener: Set = 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) { diff --git a/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts b/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts deleted file mode 100644 index d415707..0000000 --- a/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - Cache, - OptionsValidated, - OptionsWithOptionsTemplate, - transformOptions, - validateOptions, - assignDeep, - hasOwnProperty, - isEmptyObject, -} from 'support'; -import { PlainObject } from 'typings'; - -interface LifecycleBaseUpdateHints { - _force?: boolean; - _changedOptions?: OptionsValidated; -} - -export interface LifecycleBase { - _options(newOptions?: O): O; - _update(force?: boolean): void; -} - -export interface Lifecycle extends LifecycleBase { - _destruct(): void; - _onSizeChanged?(): void; - _onDirectionChanged?(directionCache: Cache): void; - _onTrinsicChanged?(widthIntrinsic: boolean, heightIntrinsicCache: Cache): void; -} - -export interface LifecycleOptionInfo { - _value: T; - _changed: boolean; -} - -export type LifecycleCheckOption = (path: string) => LifecycleOptionInfo; - -const getPropByPath = (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 = ( - defaultOptionsWithTemplate: OptionsWithOptionsTemplate>, - initialOptions: O | undefined, - updateFunction: (force: boolean, checkOption: LifecycleCheckOption) => any -): LifecycleBase => { - const { _template: optionsTemplate, _options: defaultOptions } = transformOptions>(defaultOptionsWithTemplate); - const options: Required = assignDeep( - {}, - defaultOptions, - validateOptions(initialOptions || ({} as O), optionsTemplate, null, true)._validated - ); - - const update = (hints: LifecycleBaseUpdateHints) => { - 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 }); - }, - }; -}; diff --git a/packages/overlayscrollbars/src/lifecycles/lifecycleHub.ts b/packages/overlayscrollbars/src/lifecycles/lifecycleHub.ts new file mode 100644 index 0000000..bb14471 --- /dev/null +++ b/packages/overlayscrollbars/src/lifecycles/lifecycleHub.ts @@ -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 | null, force?: boolean): void; + _destroy(): void; +} + +export interface LifecycleHub { + _options: Options; + _structureSetup: StructureSetup; +} + +const attrs = ['id', 'class', 'style', 'open']; +const directionIsRTLCacheValuesFallback: CacheValues = { + _value: false, + _previous: false, + _changed: false, +}; +const heightIntrinsicCacheValuesFallback: CacheValues = { + _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 | null, changedOptions?: OptionsValidated | 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) => { + const sizeChanged = !directionIsRTL; + runLifecycles({ + _directionIsRTL: directionIsRTL, + _sizeChanged: sizeChanged, + }); + }; + const onTrinsicChanged = (heightIntrinsic: CacheValues) => { + 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 | 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); + }, + }; +}; diff --git a/packages/overlayscrollbars/src/lifecycles/lifecycleUpdateFunction.ts b/packages/overlayscrollbars/src/lifecycles/lifecycleUpdateFunction.ts new file mode 100644 index 0000000..25a2b10 --- /dev/null +++ b/packages/overlayscrollbars/src/lifecycles/lifecycleUpdateFunction.ts @@ -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; + _heightIntrinsic: CacheValues; +} + +export type LifecycleUpdateFunction = ( + updateHints: LifecycleUpdateHints, + changedOptions?: OptionsValidated | null, + force?: boolean +) => Partial; + +export interface LifecycleOptionInfo { + readonly _value: T; + _changed: boolean; +} + +export type LifecycleCheckOption = (path: string) => LifecycleOptionInfo; + +const getPropByPath = (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 | void +): LifecycleUpdateFunction => { + return (updateHints: LifecycleUpdateHints, changedOptions?: OptionsValidated | null, force?: boolean) => { + const checkOption: LifecycleCheckOption = (path) => ({ + _value: getPropByPath(lifecycleHub._options, path), + _changed: force || getPropByPath(changedOptions, path) !== undefined, + }); + return updateFunction(!!force, updateHints, checkOption) || {}; + }; +}; diff --git a/packages/overlayscrollbars/src/lifecycles/overflowLifecycle.ts b/packages/overlayscrollbars/src/lifecycles/overflowLifecycle.ts new file mode 100644 index 0000000..9cdcd82 --- /dev/null +++ b/packages/overlayscrollbars/src/lifecycles/overflowLifecycle.ts @@ -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; + _viewportSize: WH; +} + +export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): LifecycleUpdateFunction => { + const { _host, _padding, _viewport, _content, _contentArrange } = lifecycleHub._structureSetup._targetObj; + const { _update: updateContentScrollSizeCache, _current: getCurrentContentScrollSizeCache } = createCache>( + () => scrollSize(_content || _viewport), + { _equal: equalWH } + ); + const { _update: updateOverflowAmountCache, _current: getCurrentOverflowAmountCache } = createCache, 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, + 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> = getCurrentOverflowAmountCache(); + let contentScrollSizeCache: CacheValues> = 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); + } + }); +}; diff --git a/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts b/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts index 27e40f9..40db949 100644 --- a/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts +++ b/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts @@ -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 = 'visible-hidden visible-scroll scroll hidden'; -const defaultOptionsWithTemplate: OptionsWithOptionsTemplate> = { - 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 => { - 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, { _contentScrollSize: WH; _viewportSize: WH }>( + const { _update: updatePaddingCache } = createCache(() => topRightBottomLeft(_host, 'padding'), { _equal: equalTRBL }); + const { _update: updateOverflowAmountCache } = createCache, { _contentScrollSize: WH; _viewportSize: WH }>( (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(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) => { - const { _changed, _value } = heightIntrinsicCache; + const onTrinsicChanged = (heightIntrinsic: CacheValues) => { + const { _changed, _value } = heightIntrinsic; if (_changed) { style(_content, { height: _value ? 'auto' : '100%' }); } }; return { - _options, _update, _onSizeChanged: onSizeChanged, _onTrinsicChanged: onTrinsicChanged, diff --git a/packages/overlayscrollbars/src/observers/domObserver.ts b/packages/overlayscrollbars/src/observers/domObserver.ts index 03ddfd4..e467d9d 100644 --- a/packages/overlayscrollbars/src/observers/domObserver.ts +++ b/packages/overlayscrollbars/src/observers/domObserver.ts @@ -149,7 +149,7 @@ export const createDOMObserver = ( if (isConnected) { callback([], false, true); } - }, 80) + }, 84) ); // MutationObserver diff --git a/packages/overlayscrollbars/src/observers/sizeObserver.ts b/packages/overlayscrollbars/src/observers/sizeObserver.ts index 7ebb59f..0421d95 100644 --- a/packages/overlayscrollbars/src/observers/sizeObserver.ts +++ b/packages/overlayscrollbars/src/observers/sizeObserver.ts @@ -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; + }; +} + 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) => any, + onSizeChangedCallback: (directionIsRTLCache?: CacheValues) => any, options?: SizeObserverOptions -): (() => void) => { +): SizeObserver => { const { _direction: observeDirectionChange = false, _appear: observeAppearChange = false } = options || {}; - const rtlScrollBehavior = getEnvironment()._rtlScrollBehavior; + const { _rtlScrollBehavior: rtlScrollBehavior } = getEnvironment(); const baseElements = createDOM(`
`); const sizeObserver = baseElements[0] as HTMLElement; const listenerElement = sizeObserver.firstChild as HTMLElement; - const updateResizeObserverContentRectCache = createCache(0, { + const { _update: updateResizeObserverContentRectCache } = createCache(0, { _alwaysUpdateValues: true, _equal: (currVal, newVal) => !( @@ -74,8 +86,8 @@ export const createSizeObserver = ( (!domRectHasDimensions(currVal) && domRectHasDimensions(newVal)) ), }); - const onSizeChangedCallbackProxy = (sizeChangedContext?: Cache | SizeObserverEntry[] | Event) => { - const hasDirectionCache = sizeChangedContext && isBoolean((sizeChangedContext as Cache)._value); + const onSizeChangedCallbackProxy = (sizeChangedContext?: CacheValues | SizeObserverEntry[] | Event) => { + const hasDirectionCache = sizeChangedContext && isBoolean((sizeChangedContext as CacheValues)._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)._changed; // direction scroll when DirectionCache changed, false toherwise + doDirectionScroll = (sizeChangedContext as CacheValues)._changed; // direction scroll when DirectionCache changed, false toherwise } if (observeDirectionChange) { - const rtl = hasDirectionCache ? (sizeChangedContext as Cache)._value : directionIsRTL(sizeObserver); + const rtl = hasDirectionCache ? (sizeChangedContext as CacheValues)._value : directionIsRTL(sizeObserver); scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount); scrollTop(sizeObserver, scrollAmount); } if (!skip) { - onSizeChangedCallback(hasDirectionCache ? (sizeChangedContext as Cache) : undefined); + onSizeChangedCallback(hasDirectionCache ? (sizeChangedContext as CacheValues) : undefined); } }; const offListeners: (() => void)[] = []; let appearCallback: ((...args: any) => any) | false = observeAppearChange ? onSizeChangedCallbackProxy : false; + let directionIsRTLCache: Cache | 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, + }, + }; + }, }; }; diff --git a/packages/overlayscrollbars/src/observers/trinsicObserver.ts b/packages/overlayscrollbars/src/observers/trinsicObserver.ts index 5c5a33f..f66efa4 100644 --- a/packages/overlayscrollbars/src/observers/trinsicObserver.ts +++ b/packages/overlayscrollbars/src/observers/trinsicObserver.ts @@ -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; + }; +} + export const createTrinsicObserver = ( target: HTMLElement, - onTrinsicChangedCallback: (widthIntrinsic: boolean, heightIntrinsicCache: Cache) => any -): (() => void) => { + onTrinsicChangedCallback: (heightIntrinsic: CacheValues) => any +): TrinsicObserver => { const trinsicObserver = createDOM(`
`)[0] as HTMLElement; const offListeners: (() => void)[] = []; - const updateHeightIntrinsicCache = createCache>( + const { _update: updateHeightIntrinsicCache, _current: getCurrentHeightIntrinsicCache } = createCache< + boolean, + IntersectionObserverEntry | WH + >( (ioEntryOrSize: IntersectionObserverEntry | WH) => (ioEntryOrSize! as WH).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), + }; + }, }; }; diff --git a/packages/overlayscrollbars/src/options/index.ts b/packages/overlayscrollbars/src/options/index.ts index d4f6692..7a2bfaf 100644 --- a/packages/overlayscrollbars/src/options/index.ts +++ b/packages/overlayscrollbars/src/options/index.ts @@ -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 | 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 | 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 { diff --git a/packages/overlayscrollbars/src/options/options.ts b/packages/overlayscrollbars/src/options/options.ts index 21483ee..5577216 100644 --- a/packages/overlayscrollbars/src/options/options.ts +++ b/packages/overlayscrollbars/src/options/options.ts @@ -8,15 +8,13 @@ import { } from 'support/options'; import { ResizeBehavior, OverflowBehavior, VisibilityBehavior, AutoHideBehavior, Options } from 'options'; -const classNameAllowedValues: OptionsTemplateValue = [oTypes.string, oTypes.null]; const numberAllowedValues: OptionsTemplateValue = oTypes.number; -const booleanNullAllowedValues: OptionsTemplateValue = [oTypes.boolean, oTypes.null]; -const stringArrayNullAllowedValues: OptionsTemplateValue | null> = [oTypes.string, oTypes.array, oTypes.null]; +const stringArrayNullAllowedValues: OptionsTemplateValue | null> = [oTypes.string, oTypes.array, oTypes.null]; const booleanTrueTemplate: OptionsWithOptionsTemplateValue = [true, oTypes.boolean]; const booleanFalseTemplate: OptionsWithOptionsTemplateValue = [false, oTypes.boolean]; -const callbackTemplate: OptionsWithOptionsTemplateValue = [null, [oTypes.function, oTypes.null]]; +// const callbackTemplate: OptionsWithOptionsTemplateValue = [null, [oTypes.function, oTypes.null]]; const resizeAllowedValues: OptionsTemplateValue = 'none both horizontal vertical'; -const overflowBehaviorAllowedValues: OptionsTemplateValue = 'visible-hidden visible-scroll scroll hidden'; +const overflowAllowedValues: OptionsTemplateValue = 'visible-hidden visible-scroll scroll hidden'; const scrollbarsVisibilityAllowedValues: OptionsTemplateValue = 'visible hidden auto'; const scrollbarsAutoHideAllowedValues: OptionsTemplateValue = 'never scroll leavemove'; @@ -36,37 +34,36 @@ const scrollbarsAutoHideAllowedValues: OptionsTemplateValue = * Property "b" has a default value of 250 and it can be number */ const defaultOptionsWithTemplate: OptionsWithOptionsTemplate> = { - 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> onHostSizeChanged: callbackTemplate, // null || function onUpdated: callbackTemplate, // null || function }, + */ }; export const { _template: optionsTemplate, _options: defaultOptions } = transformOptions(defaultOptionsWithTemplate); diff --git a/packages/overlayscrollbars/src/overlayscrollbars.scss b/packages/overlayscrollbars/src/overlayscrollbars.scss index 26e238d..7aa1e77 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars.scss +++ b/packages/overlayscrollbars/src/overlayscrollbars.scss @@ -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; +} diff --git a/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts b/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts index dcc538c..8047aab 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts @@ -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[] = []; - const { _host, _viewport, _content } = structureSetup._targetObj; - - push(lifecycles, createStructureLifecycle(structureSetup._targetObj)); - - // eslint-disable-next-line - const onSizeChanged = (directionCache?: Cache) => { - if (directionCache) { - each(lifecycles, (lifecycle) => { - lifecycle._onDirectionChanged && lifecycle._onDirectionChanged(directionCache); - }); - } else { - each(lifecycles, (lifecycle) => { - lifecycle._onSizeChanged && lifecycle._onSizeChanged(); - }); - } - }; - const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsicCache: Cache) => { - 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 = assignDeep( + {}, + defaultOptions, + validateOptions(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 }; diff --git a/packages/overlayscrollbars/src/setups/structureSetup.ts b/packages/overlayscrollbars/src/setups/structureSetup.ts index cdb3c0d..c1019f9 100644 --- a/packages/overlayscrollbars/src/setups/structureSetup.ts +++ b/packages/overlayscrollbars/src/setups/structureSetup.ts @@ -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> { _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, diff --git a/packages/overlayscrollbars/src/support/cache/cache.ts b/packages/overlayscrollbars/src/support/cache/cache.ts index 7c0015f..fe50b46 100644 --- a/packages/overlayscrollbars/src/support/cache/cache.ts +++ b/packages/overlayscrollbars/src/support/cache/cache.ts @@ -1,7 +1,7 @@ -export interface Cache { +export interface CacheValues { readonly _value?: T; readonly _previous?: T; - readonly _changed: boolean; + _changed: boolean; } export interface CacheOptions { @@ -13,7 +13,14 @@ export interface CacheOptions { _alwaysUpdateValues?: boolean; } -export type CacheUpdate = undefined extends C ? (force?: boolean | 0, context?: C) => Cache : (force: boolean | 0, context: C) => Cache; +export interface Cache { + _current: (force?: boolean) => CacheValues; + _update: CacheUpdate; +} + +export type CacheUpdate = undefined extends C + ? (force?: boolean | 0, context?: C) => CacheValues + : (force: boolean | 0, context: C) => CacheValues; export type UpdateCachePropFunction = undefined extends C ? (context?: C, current?: T, previous?: T) => T @@ -23,7 +30,7 @@ export type UpdateCachePropFunction = undefined extends C export type EqualCachePropFunction = (currentVal?: T, newVal?: T) => boolean; -export const createCache = (update: UpdateCachePropFunction, options?: CacheOptions): CacheUpdate => { +export const createCache = (update: UpdateCachePropFunction, options?: CacheOptions): Cache => { const { _equal, _initialValue, _alwaysUpdateValues } = options || {}; let _value: T | undefined = _initialValue; let _previous: T | undefined; @@ -48,5 +55,12 @@ export const createCache = (update: UpdateCachePropFunction; - return cacheUpdate; + return { + _update: cacheUpdate, + _current: (force?: boolean) => ({ + _value, + _previous, + _changed: !!force, + }), + }; }; diff --git a/packages/overlayscrollbars/src/support/dom/events.ts b/packages/overlayscrollbars/src/support/dom/events.ts index 89606f6..4fab1d8 100644 --- a/packages/overlayscrollbars/src/support/dom/events.ts +++ b/packages/overlayscrollbars/src/support/dom/events.ts @@ -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 = (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 = ( + 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); diff --git a/packages/overlayscrollbars/src/support/options/index.ts b/packages/overlayscrollbars/src/support/options/index.ts index 710a5b7..cb0964a 100644 --- a/packages/overlayscrollbars/src/support/options/index.ts +++ b/packages/overlayscrollbars/src/support/options/index.ts @@ -39,7 +39,7 @@ type OptionsTemplateTypeMap = { __TPL_boolean_TYPE__: boolean; __TPL_number_TYPE__: number; __TPL_string_TYPE__: string; - __TPL_array_TYPE__: Array; + __TPL_array_TYPE__: Array | ReadonlyArray; __TPL_function_TYPE__: Func; __TPL_null_TYPE__: null; __TPL_object_TYPE__: Record; diff --git a/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.browser.ts b/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.browser.ts index 5a7eccb..1aad378 100644 --- a/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.browser.ts +++ b/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.browser.ts @@ -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 = { w: 0, h: 0 }; + const dragStartPosition: XY = { 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')!); diff --git a/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.scss b/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.scss index 0fe23c2..c242343 100644 --- a/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.scss +++ b/packages/overlayscrollbars/tests/browser/lifecycles/structureLifecycle/index.scss @@ -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; +} diff --git a/packages/overlayscrollbars/tests/jsdom/support/cache/cache.test.ts b/packages/overlayscrollbars/tests/jsdom/support/cache/cache.test.ts index 17abad6..4f989dd 100644 --- a/packages/overlayscrollbars/tests/jsdom/support/cache/cache.test.ts +++ b/packages/overlayscrollbars/tests/jsdom/support/cache/cache.test.ts @@ -15,15 +15,17 @@ const createUpdater = (updaterReturn: (i: number) => T) => { describe('cache', () => { test('creates and updates cache', () => { const [fn, updater] = createUpdater((i) => `${i}`); - const update = createCache(updater); + const { _update, _current } = createCache(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(0); + const { _update } = createCache(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(updater, { _equal: () => true }); + const { _update, _current } = createCache(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(updater, { _equal: () => false }); + const { _update, _current } = createCache(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(updater, { _equal: (a, b) => a?.a === b?.a && a?.b === b?.b }); + const { _update } = createCache(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(updater, { _initialValue: 0 }); + const { _update, _current } = createCache(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(updater, { _initialValue: obj, _equal: (a, b) => a?.a === b?.a && a?.b === b?.b }); + const { _update } = createCache(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(updater, { _alwaysUpdateValues: true, _equal: () => true }); + const { _update } = createCache(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(0, { _alwaysUpdateValues: true }); + const { _update, _current } = createCache(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(updater); + const { _update, _current } = createCache(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(updater, { _initialValue: obj }); + const { _update } = createCache(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(updater); + const { _update, _current } = createCache(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');