diff --git a/packages/overlayscrollbars/src/autoUpdateLoop/autoUpdateLoop.ts b/packages/overlayscrollbars/src/autoUpdateLoop/autoUpdateLoop.ts new file mode 100644 index 0000000..8e86797 --- /dev/null +++ b/packages/overlayscrollbars/src/autoUpdateLoop/autoUpdateLoop.ts @@ -0,0 +1,85 @@ +import { rAF, cAF, isEmptyArray, indexOf, createCache, runEach } from 'support'; +import { getEnvironment } from 'environment'; + +export interface AutoUpdateLoop { + _add(fn: (delta: number) => any): () => void; + _interval(newInterval: number): () => void; + _interval(): number; +} + +const defaultLoopInterval = 33; +let autoUpdateLoopInstance: AutoUpdateLoop; + +const createAutoUpdateLoop = (): AutoUpdateLoop => { + let loopIsRunning = false; + let loopInterval = defaultLoopInterval; + let loopId: number | undefined; + const intervals: number[] = []; + const loopFunctions: Array<(...args: any) => any> = []; + const updateLoopInterval = () => { + loopInterval = isEmptyArray(intervals) ? defaultLoopInterval : Math.min.apply(null, intervals); + }; + const updateTimeCache = createCache((ctx) => ctx || performance.now(), { + _initialValue: performance.now(), + _equal: (currTime, newTime) => { + const delta = newTime! - currTime!; + return delta < loopInterval; + }, + }); + const loop = (newTime?: number) => { + /* istanbul ignore next */ + if (!isEmptyArray(loopFunctions) && loopIsRunning) { + loopId = rAF!(loop); + const { _changed, _value, _previous } = updateTimeCache(0, newTime); + if (_changed) { + runEach(loopFunctions, _value! - _previous!); + } + } + }; + function interval(): number; + function interval(newInterval: number): () => void; + function interval(newInterval?: number): number | (() => void) { + if (newInterval) { + intervals.push(newInterval); + updateLoopInterval(); + + return () => { + intervals.splice(indexOf(intervals, newInterval), 1); + updateLoopInterval(); + }; + } + return loopInterval; + } + + return { + _add: (fn) => { + loopFunctions.push(fn); + + if (!loopIsRunning && !isEmptyArray(loopFunctions)) { + getEnvironment()._autoUpdateLoop = loopIsRunning = true; + + updateTimeCache(true); + loop(); + } + + return () => { + loopFunctions.splice(indexOf(loopFunctions, fn), 1); + + if (isEmptyArray(loopFunctions) && loopIsRunning) { + getEnvironment()._autoUpdateLoop = loopIsRunning = false; + + cAF!(loopId!); + loopId = undefined; + } + }; + }, + _interval: interval, + }; +}; + +export const getAutoUpdateLoop = (): AutoUpdateLoop => { + if (!autoUpdateLoopInstance) { + autoUpdateLoopInstance = createAutoUpdateLoop(); + } + return autoUpdateLoopInstance; +}; diff --git a/packages/overlayscrollbars/src/autoUpdateLoop/index.ts b/packages/overlayscrollbars/src/autoUpdateLoop/index.ts new file mode 100644 index 0000000..0dd1fa4 --- /dev/null +++ b/packages/overlayscrollbars/src/autoUpdateLoop/index.ts @@ -0,0 +1 @@ +export * from 'autoUpdateLoop/autoUpdateLoop'; diff --git a/packages/overlayscrollbars/src/classnames.ts b/packages/overlayscrollbars/src/classnames.ts new file mode 100644 index 0000000..a196984 --- /dev/null +++ b/packages/overlayscrollbars/src/classnames.ts @@ -0,0 +1,18 @@ +export const classNameEnvironment = 'os-environment'; +export const classNameEnvironmentFlexboxGlue = `${classNameEnvironment}-flexbox-glue`; +export const classNameEnvironmentFlexboxGlueMax = `${classNameEnvironmentFlexboxGlue}-max`; + +export const classNameHost = 'os-host'; +export const classNamePadding = 'os-padding'; +export const classNameViewport = 'os-viewport'; +export const classNameContent = 'os-content'; +export const classNameViewportScrollbarStyling = `${classNameViewport}-scrollbar-styled`; + +export const classNameSizeObserver = 'os-size-observer'; +export const classNameSizeObserverAppear = `${classNameSizeObserver}-appear`; +export const classNameSizeObserverListener = `${classNameSizeObserver}-listener`; +export const classNameSizeObserverListenerScroll = `${classNameSizeObserverListener}-scroll`; +export const classNameSizeObserverListenerItem = `${classNameSizeObserverListener}-item`; +export const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`; + +export const classNameTrinsicObserver = 'os-trinsic-observer'; diff --git a/packages/overlayscrollbars/src/environment/environment.ts b/packages/overlayscrollbars/src/environment/environment.ts index 39cdd2f..89ca6a6 100644 --- a/packages/overlayscrollbars/src/environment/environment.ts +++ b/packages/overlayscrollbars/src/environment/environment.ts @@ -14,6 +14,12 @@ import { runEach, equalWH, } from 'support'; +import { + classNameEnvironment, + classNameEnvironmentFlexboxGlue, + classNameEnvironmentFlexboxGlueMax, + classNameViewportScrollbarStyling, +} from 'classnames'; export type OnEnvironmentChanged = (env: Environment) => void; export interface Environment { @@ -29,9 +35,6 @@ export interface Environment { let environmentInstance: Environment; const { abs, round } = Math; -const environmentElmId = 'os-environment'; -const classNameFlexboxGlue = 'flexbox-glue'; -const classNameFlexboxGlueMax = `${classNameFlexboxGlue}-max`; const getNativeScrollbarSize = (body: HTMLElement, measureElm: HTMLElement): XY => { appendChildren(body, measureElm); @@ -46,7 +49,7 @@ const getNativeScrollbarSize = (body: HTMLElement, measureElm: HTMLElement): XY const getNativeScrollbarStyling = (testElm: HTMLElement): boolean => { let result = false; - addClass(testElm, 'os-viewport-scrollbar-styled'); + addClass(testElm, classNameViewportScrollbarStyling); try { result = style(testElm, 'scrollbar-width') === 'none' || window.getComputedStyle(testElm, '::-webkit-scrollbar').getPropertyValue('display') === 'none'; @@ -83,12 +86,12 @@ const getRtlScrollBehavior = (parentElm: HTMLElement, childElm: HTMLElement): { }; const getFlexboxGlue = (parentElm: HTMLElement, childElm: HTMLElement): boolean => { - addClass(parentElm, classNameFlexboxGlue); + addClass(parentElm, classNameEnvironmentFlexboxGlue); const minOffsetsizeParent = offsetSize(parentElm); const minOffsetsize = offsetSize(childElm); const supportsMin = equalWH(minOffsetsize, minOffsetsizeParent); - addClass(parentElm, classNameFlexboxGlueMax); + addClass(parentElm, classNameEnvironmentFlexboxGlueMax); const maxOffsetsizeParent = offsetSize(parentElm); const maxOffsetsize = offsetSize(childElm); const supportsMax = equalWH(maxOffsetsize, maxOffsetsizeParent); @@ -114,7 +117,7 @@ const diffBiggerThanOne = (valOne: number, valTwo: number): boolean => { const createEnvironment = (): Environment => { const { body } = document; - const envDOM = createDOM(`
`); + const envDOM = createDOM(`
`); const envElm = envDOM[0] as HTMLElement; const envChildElm = envElm.firstChild as HTMLElement; diff --git a/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts b/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts index 16cecac..d05608f 100644 --- a/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts +++ b/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts @@ -5,11 +5,16 @@ import { createCache, topRightBottomLeft, TRBL, + WH, + XY, equalTRBL, + equalXY, optionsTemplateTypes as oTypes, OptionsTemplateValue, style, OptionsWithOptionsTemplate, + scrollSize, + offsetSize, } from 'support'; import { OSTargetObject } from 'typings'; import { createLifecycleBase, Lifecycle } from 'lifecycles/lifecycleBase'; @@ -33,11 +38,6 @@ const defaultOptionsWithTemplate: OptionsWithOptionsTemplate topRightBottomLeft(host, 'padding'), { _equal: equalTRBL }); + const 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), + }), + { _equal: equalXY } + ); const { _options, _update } = createLifecycleBase(defaultOptionsWithTemplate, initialOptions, (force, checkOption) => { const { _value: paddingAbsolute, _changed: paddingAbsoluteChanged } = checkOption('paddingAbsolute'); @@ -75,11 +82,6 @@ export const createStructureLifecycle = ( paddingStyle.l = -padding!.l; } - if (!supportsScrollbarStyling) { - paddingStyle.r -= env._nativeScrollbarSize.y; - paddingStyle.b -= env._nativeScrollbarSize.x; - } - style(paddingElm, { top: paddingStyle.t, left: paddingStyle.l, @@ -88,6 +90,59 @@ export const createStructureLifecycle = ( 'max-width': `calc(100% + ${paddingStyle.r * -1}px)`, }); } + + const viewportOffsetSize = offsetSize(paddingElm); + const contentClientSize = offsetSize(content); + const contentScrollSize = scrollSize(content); + const overflowAmuntCache = updateOverflowAmountCache(force, { + _contentScrollSize: contentScrollSize, + _viewportSize: { + w: viewportOffsetSize.w + Math.max(0, contentClientSize.w - contentScrollSize.w), + h: viewportOffsetSize.h + Math.max(0, contentClientSize.h - contentScrollSize.h), + }, + }); + const { _value: overflowAmount, _changed: overflowAmountChanged } = overflowAmuntCache; + + console.log('overflowAmount', overflowAmount); + console.log('overflowAmountChanged', overflowAmountChanged); + + /* + var setOverflowVariables = function (horizontal) { + var scrollbarVars = getScrollbarVars(horizontal); + var scrollbarVarsInverted = getScrollbarVars(!horizontal); + var xyI = scrollbarVarsInverted._x_y; + var xy = scrollbarVars._x_y; + var wh = scrollbarVars._w_h; + var widthHeight = scrollbarVars._width_height; + var scrollMax = _strScroll + scrollbarVars._Left_Top + 'Max'; + var fractionalOverflowAmount = viewportRect[widthHeight] ? MATH.abs(viewportRect[widthHeight] - _viewportSize[wh]) : 0; + var checkFractionalOverflowAmount = previousOverflowAmount && previousOverflowAmount[xy] > 0 && _viewportElementNative[scrollMax] === 0; + overflowBehaviorIsVS[xy] = overflowBehavior[xy] === 'v-s'; + overflowBehaviorIsVH[xy] = overflowBehavior[xy] === 'v-h'; + overflowBehaviorIsS[xy] = overflowBehavior[xy] === 's'; + overflowAmount[xy] = MATH.max(0, MATH.round((contentScrollSize[wh] - _viewportSize[wh]) * 100) / 100); + overflowAmount[xy] *= + hideOverflowForceTextarea || (checkFractionalOverflowAmount && fractionalOverflowAmount > 0 && fractionalOverflowAmount < 1) ? 0 : 1; + hasOverflow[xy] = overflowAmount[xy] > 0; + + //hideOverflow: + //x || y : true === overflow is hidden by "overflow: scroll" OR "overflow: hidden" + //xs || ys : true === overflow is hidden by "overflow: scroll" + hideOverflow[xy] = + overflowBehaviorIsVS[xy] || overflowBehaviorIsVH[xy] + ? hasOverflow[xyI] && !overflowBehaviorIsVS[xyI] && !overflowBehaviorIsVH[xyI] + : hasOverflow[xy]; + hideOverflow[xy + 's'] = hideOverflow[xy] ? overflowBehaviorIsS[xy] || overflowBehaviorIsVS[xy] : false; + + canScroll[xy] = hasOverflow[xy] && hideOverflow[xy + 's']; + }; +*/ + /* + if (!supportsScrollbarStyling) { + paddingStyle.r -= env._nativeScrollbarSize.y; + paddingStyle.b -= env._nativeScrollbarSize.x; + } + */ }); const onSizeChanged = () => { diff --git a/packages/overlayscrollbars/src/observers/domObserver.ts b/packages/overlayscrollbars/src/observers/domObserver.ts new file mode 100644 index 0000000..748de7b --- /dev/null +++ b/packages/overlayscrollbars/src/observers/domObserver.ts @@ -0,0 +1,89 @@ +import { each, indexOf, isString, MutationObserverConstructor, isEmptyArray, liesBetween } from 'support'; +import { classNameHost, classNameContent } from 'classnames'; + +export interface DOMObserverOptions { + _observeContent?: boolean; + _attributes?: string[]; +} +export interface DOMObserver { + _disconnect: () => void; + _update: () => void; +} + +const styleChangingAttributes = ['id', 'class', 'style', 'open']; +const mutationObserverAttrsTextarea = ['wrap', 'cols', 'rows']; + +const isUnknownMutation = ( + attributeName: string | null, + type: MutationRecordType, + observeContent?: boolean, + target?: Node, + mutationTarget?: Node +) => { + const isAttributesType = type === 'attributes'; + const targetIsMutationTarget = target === mutationTarget; + const styleChangingAttrChanged = indexOf(styleChangingAttributes, attributeName) > -1; + const contentChanged = observeContent && !isAttributesType; + const contentAttrChanged = + observeContent && + isAttributesType && + styleChangingAttrChanged && + !targetIsMutationTarget && + !liesBetween(mutationTarget as Element | undefined, `.${classNameHost}`, `.${classNameContent}`); + const targetAttrChanged = isAttributesType && styleChangingAttrChanged && targetIsMutationTarget && !observeContent; + + return contentChanged || contentAttrChanged || targetAttrChanged; +}; + +export const createDOMObserver = ( + target: HTMLElement, + callback: (changedTargetAttrs: string[], styleChanged: boolean, contentChanged: boolean) => any, + options?: DOMObserverOptions +): DOMObserver => { + const { _observeContent, _attributes } = options || {}; + + // MutationObserver + const observedAttributes = (_attributes || []).concat(_observeContent ? styleChangingAttributes : mutationObserverAttrsTextarea); + const observerCallback = (mutations: MutationRecord[]) => { + let styleChanged = false; + let contentChanged = false; + const changedTargetAttrs: string[] = []; + each(mutations, (mutation) => { + const { attributeName, target: mutationTarget, type } = mutation; + + styleChanged = styleChanged || isUnknownMutation(attributeName, type); + + if (_observeContent) { + contentChanged = contentChanged || isUnknownMutation(attributeName, type, true, target, mutationTarget); + } + if (isString(attributeName) && target === mutationTarget) { + changedTargetAttrs.push(attributeName); + } + }); + + if (!isEmptyArray(changedTargetAttrs) || styleChanged || contentChanged) { + callback(changedTargetAttrs, styleChanged, contentChanged); + } + }; + const mutationObserver: MutationObserver = new MutationObserverConstructor!(observerCallback); + + const connect = () => { + mutationObserver.observe(target, { + attributes: true, + attributeOldValue: true, + subtree: _observeContent, + childList: _observeContent, + characterData: _observeContent, + attributeFilter: observedAttributes, + }); + }; + + connect(); + + return { + _disconnect: mutationObserver.disconnect, + _update: () => { + observerCallback(mutationObserver.takeRecords()); + }, + }; +}; diff --git a/packages/overlayscrollbars/src/observers/sizeObserver.ts b/packages/overlayscrollbars/src/observers/sizeObserver.ts index 4ab4704..bcf6d97 100644 --- a/packages/overlayscrollbars/src/observers/sizeObserver.ts +++ b/packages/overlayscrollbars/src/observers/sizeObserver.ts @@ -17,22 +17,24 @@ import { addClass, isString, equalWH, + cAF, + rAF, } from 'support'; import { CSSDirection } from 'typings'; import { getEnvironment } from 'environment'; +import { + classNameSizeObserver, + classNameSizeObserverAppear, + classNameSizeObserverListener, + classNameSizeObserverListenerScroll, + classNameSizeObserverListenerItem, + classNameSizeObserverListenerItemFinal, +} from 'classnames'; const animationStartEventName = 'animationstart'; const scrollEventName = 'scroll'; const scrollAmount = 3333333; const ResizeObserverConstructor = jsAPI('ResizeObserver'); -const classNameSizeObserver = 'os-size-observer'; -const classNameSizeObserverAppear = `${classNameSizeObserver}-appear`; -const classNameSizeObserverListener = `${classNameSizeObserver}-listener`; -const classNameSizeObserverListenerScroll = `${classNameSizeObserverListener}-scroll`; -const classNameSizeObserverListenerItem = `${classNameSizeObserverListener}-item`; -const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`; -const cAF = cancelAnimationFrame; -const rAF = requestAnimationFrame; const getDirection = (elm: HTMLElement): CSSDirection => style(elm, 'direction') as CSSDirection; export type SizeObserverOptions = { _direction?: boolean; _appear?: boolean }; @@ -95,8 +97,8 @@ export const createSizeObserver = ( isDirty = !scrollEvent || !equalWH(currSize, cacheSize); if (scrollEvent && isDirty && !rAFId) { - cAF(rAFId); - rAFId = rAF(onResized); + cAF!(rAFId); + rAFId = rAF!(onResized); } else if (!scrollEvent) { onResized(); } diff --git a/packages/overlayscrollbars/src/observers/trinsicObserver.ts b/packages/overlayscrollbars/src/observers/trinsicObserver.ts index dad9482..354c764 100644 --- a/packages/overlayscrollbars/src/observers/trinsicObserver.ts +++ b/packages/overlayscrollbars/src/observers/trinsicObserver.ts @@ -1,8 +1,6 @@ -import { WH, Cache, createDOM, offsetSize, jsAPI, runEach, prependChildren, removeElements, createCache } from 'support'; +import { WH, Cache, createDOM, offsetSize, runEach, prependChildren, removeElements, createCache, IntersectionObserverConstructor } from 'support'; import { createSizeObserver } from 'observers/sizeObserver'; - -const classNameTrinsicObserver = 'os-trinsic-observer'; -const IntersectionObserverConstructor = jsAPI('IntersectionObserver'); +import { classNameTrinsicObserver } from 'classnames'; export const createTrinsicObserver = ( target: HTMLElement, diff --git a/packages/overlayscrollbars/src/overlayscrollbars.scss b/packages/overlayscrollbars/src/overlayscrollbars.scss index e871b93..26e238d 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars.scss +++ b/packages/overlayscrollbars/src/overlayscrollbars.scss @@ -2,7 +2,7 @@ @import './trinsicobserver.scss'; @import './structurelifecycle.scss'; -#os-environment { +.os-environment { position: fixed; opacity: 0; visibility: hidden; @@ -16,7 +16,7 @@ margin: 10px 0; } - &.flexbox-glue { + &.os-environment-flexbox-glue { display: flex; flex-direction: row; flex-wrap: nowrap; @@ -35,7 +35,7 @@ } } - &.flexbox-glue-max { + &.os-environment-flexbox-glue-max { max-height: 200px; div { diff --git a/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts b/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts index fed5871..61eab4f 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts @@ -4,11 +4,7 @@ import { Cache, appendChildren, addClass, contents, is, isHTMLElement, createDiv import { createSizeObserver } from 'observers/sizeObserver'; import { createTrinsicObserver } from 'observers/trinsicObserver'; import { Lifecycle } from 'lifecycles/lifecycleBase'; - -const classNameHost = 'os-host'; -const classNamePadding = 'os-padding'; -const classNameViewport = 'os-viewport'; -const classNameContent = 'os-content'; +import { classNameHost, classNamePadding, classNameViewport, classNameContent } from 'classnames'; const normalizeTarget = (target: OSTarget): OSTargetObject => { if (isHTMLElement(target)) { diff --git a/packages/overlayscrollbars/src/support/cache/cache.ts b/packages/overlayscrollbars/src/support/cache/cache.ts index ea52fc1..fa754c3 100644 --- a/packages/overlayscrollbars/src/support/cache/cache.ts +++ b/packages/overlayscrollbars/src/support/cache/cache.ts @@ -13,20 +13,20 @@ export type CacheUpdate = (force?: boolean | 0, context?: C) => Cache; export type UpdateCachePropFunction = (context?: C, current?: T, previous?: T) => T; -export type EqualCachePropFunction = (a?: T, b?: T) => boolean; +export type EqualCachePropFunction = (currentVal?: T, newVal?: T) => boolean; export const createCache = (update: UpdateCachePropFunction, options?: CacheOptions): CacheUpdate => { const { _equal, _initialValue } = options || {}; let _value: T | undefined = _initialValue; let _previous: T | undefined; return (force, context) => { - const prev = _value; + const curr = _value; const newVal = update(context, _value, _previous); - const changed = force || (_equal ? !_equal(prev, newVal) : prev !== newVal); + const changed = force || (_equal ? !_equal(curr, newVal) : curr !== newVal); if (changed) { _value = newVal; - _previous = prev; + _previous = curr; } return { diff --git a/packages/overlayscrollbars/src/support/compatibility/apis.ts b/packages/overlayscrollbars/src/support/compatibility/apis.ts index 3724de2..d080c88 100644 --- a/packages/overlayscrollbars/src/support/compatibility/apis.ts +++ b/packages/overlayscrollbars/src/support/compatibility/apis.ts @@ -1,3 +1,7 @@ import { jsAPI } from 'support/compatibility/vendors'; -export const resizeObserver: any | undefined = jsAPI('ResizeObserver'); +export const MutationObserverConstructor = jsAPI('MutationObserver'); +export const IntersectionObserverConstructor = jsAPI('IntersectionObserver'); +export const ResizeObserverConstructor: any | undefined = jsAPI('ResizeObserver'); +export const cAF = jsAPI('cancelAnimationFrame'); +export const rAF = jsAPI('requestAnimationFrame'); diff --git a/packages/overlayscrollbars/src/support/dom/dimensions.ts b/packages/overlayscrollbars/src/support/dom/dimensions.ts index 5edb97a..9ae61f0 100644 --- a/packages/overlayscrollbars/src/support/dom/dimensions.ts +++ b/packages/overlayscrollbars/src/support/dom/dimensions.ts @@ -18,8 +18,8 @@ export const windowSize = (): WH => ({ }); /** - * Returns the offset- width and height of the passed element. If the element is null the width and height values are 0. - * @param elm The element of which the offset- width and height shall be returned. + * Returns the scroll- width and height of the passed element. If the element is null the width and height values are 0. + * @param elm The element of which the scroll- width and height shall be returned. */ export const offsetSize = (elm: HTMLElement | null): WH => elm @@ -41,6 +41,18 @@ export const clientSize = (elm: HTMLElement | null): WH => } : zeroObj; +/** + * Returns the client- width and height of the passed element. If the element is null the width and height values are 0. + * @param elm The element of which the client- width and height shall be returned. + */ +export const scrollSize = (elm: HTMLElement | null): WH => + elm + ? { + w: elm.scrollWidth, + h: elm.scrollHeight, + } + : zeroObj; + /** * Returns the BoundingClientRect of the passed element. * @param elm The element of which the BoundingClientRect shall be returned. diff --git a/packages/overlayscrollbars/src/support/dom/manipulation.ts b/packages/overlayscrollbars/src/support/dom/manipulation.ts index fc8831f..db4fd39 100644 --- a/packages/overlayscrollbars/src/support/dom/manipulation.ts +++ b/packages/overlayscrollbars/src/support/dom/manipulation.ts @@ -69,7 +69,7 @@ export const prependChildren = (node: Node | null, children: NodeCollection): vo * @param insertedNodes The Nodes which shall be inserted. */ export const insertBefore = (node: Node | null, insertedNodes: NodeCollection): void => { - before(parent(node), node, insertedNodes); + before(parent(node as Element), node, insertedNodes); }; /** @@ -78,7 +78,7 @@ export const insertBefore = (node: Node | null, insertedNodes: NodeCollection): * @param insertedNodes The Nodes which shall be inserted. */ export const insertAfter = (node: Node | null, insertedNodes: NodeCollection): void => { - before(parent(node), node && node.nextSibling, insertedNodes); + before(parent(node as Element), node && node.nextSibling, insertedNodes); }; /** @@ -89,7 +89,7 @@ export const removeElements = (nodes: NodeCollection): void => { if (isArrayLike(nodes)) { each(from(nodes), (e) => removeElements(e)); } else if (nodes) { - const parentElm = parent(nodes); + const parentElm = parent(nodes as Element); if (parentElm) { parentElm.removeChild(nodes); } diff --git a/packages/overlayscrollbars/src/support/dom/traversal.ts b/packages/overlayscrollbars/src/support/dom/traversal.ts index e53e0fd..a6f4fe0 100644 --- a/packages/overlayscrollbars/src/support/dom/traversal.ts +++ b/packages/overlayscrollbars/src/support/dom/traversal.ts @@ -1,22 +1,16 @@ import { each, from } from 'support/utils/array'; -const matches = (elm: Element | null, selector: string): boolean => { - if (elm) { - /* istanbul ignore next */ - // eslint-disable-next-line - // @ts-ignore - const fn = Element.prototype.matches || Element.prototype.msMatchesSelector; - return fn.call(elm, selector); - } - return false; -}; +type InputElementType = Element | null | undefined; +type OutputElementType = Element | null; + +const elmPrototype = Element.prototype; /** * Find all elements with the passed selector, outgoing (and including) the passed element or the document if no element was provided. * @param selector The selector which has to be searched by. * @param elm The element from which the search shall be outgoing. */ -export const find = (selector: string, elm?: Element | null): ReadonlyArray => { +const find = (selector: string, elm?: InputElementType): ReadonlyArray => { const arr: Array = []; each((elm || document).querySelectorAll(selector), (e: Element) => { @@ -31,26 +25,35 @@ export const find = (selector: string, elm?: Element | null): ReadonlyArray (elm || document).querySelector(selector); +const findFirst = (selector: string, elm?: InputElementType): OutputElementType => (elm || document).querySelector(selector); /** * Determines whether the passed element is matching with the passed selector. * @param elm The element which has to be compared with the passed selector. * @param selector The selector which has to be compared with the passed element. Additional selectors: ':visible' and ':hidden'. */ -export const is = (elm: Element | null, selector: string): boolean => matches(elm, selector); +const is = (elm: InputElementType, selector: string): boolean => { + if (elm) { + /* istanbul ignore next */ + // eslint-disable-next-line + // @ts-ignore + const fn = elmPrototype.matches || elmPrototype.msMatchesSelector; + return fn.call(elm, selector); + } + return false; +}; /** * Returns the children (no text-nodes or comments) of the passed element which are matching the passed selector. An empty array is returned if the passed element is null. * @param elm The element of which the children shall be returned. * @param selector The selector which must match with the children elements. */ -export const children = (elm: Element | null, selector?: string): ReadonlyArray => { +const children = (elm: InputElementType, selector?: string): ReadonlyArray => { const childs: Array = []; each(elm && elm.children, (child: Element) => { if (selector) { - if (matches(child, selector)) { + if (is(child, selector)) { childs.push(child); } } else { @@ -65,10 +68,47 @@ export const children = (elm: Element | null, selector?: string): ReadonlyArray< * Returns the childNodes (incl. text-nodes or comments etc.) of the passed element. An empty array is returned if the passed element is null. * @param elm The element of which the childNodes shall be returned. */ -export const contents = (elm: Element | null): ReadonlyArray => (elm ? from(elm.childNodes) : []); +const contents = (elm: InputElementType): ReadonlyArray => (elm ? from(elm.childNodes) : []); /** * Returns the parent element of the passed element, or null if the passed element is null. * @param elm The element of which the parent element shall be returned. */ -export const parent = (elm: Node | null): Node | null => (elm ? elm.parentElement : null); +const parent = (elm: InputElementType): OutputElementType => (elm ? elm.parentElement : null); + +const closest = (elm: InputElementType, selector: string): OutputElementType => { + if (elm) { + // eslint-disable-next-line + // @ts-ignore + if (elmPrototype.closest) { + return elm.closest(selector); + } + do { + if (is(elm, selector)) { + return elm; + } + elm = parent(elm); + } while (elm !== null && elm.nodeType === 1); + } + + return null; +}; + +/** + * Determines whether the given element lies between two selectors in the DOM. + * @param elm The element. + * @param highBoundarySelector The high boundary selector. + * @param deepBoundarySelector The deep boundary selector. + */ +const liesBetween = (elm: InputElementType, highBoundarySelector: string, deepBoundarySelector: string): boolean => { + const closestHighBoundaryElm = closest(elm, highBoundarySelector); + const closestDeepBoundaryElm = findFirst(deepBoundarySelector, closestHighBoundaryElm); + + return closestHighBoundaryElm && closestDeepBoundaryElm + ? closestHighBoundaryElm === elm || + closestDeepBoundaryElm === elm || + closest(closest(elm, deepBoundarySelector), highBoundarySelector) !== closestHighBoundaryElm + : false; +}; + +export { find, findFirst, is, children, contents, parent, liesBetween }; diff --git a/packages/overlayscrollbars/src/support/utils/array.ts b/packages/overlayscrollbars/src/support/utils/array.ts index e592ca7..95021de 100644 --- a/packages/overlayscrollbars/src/support/utils/array.ts +++ b/packages/overlayscrollbars/src/support/utils/array.ts @@ -15,23 +15,26 @@ export function each( callback: (value: T, indexOrKey: number, source: Array) => boolean | void ): Array | ReadonlyArray; export function each( - array: Array | ReadonlyArray | null, + array: Array | ReadonlyArray | null | undefined, callback: (value: T, indexOrKey: number, source: Array) => boolean | void -): Array | ReadonlyArray | null; +): Array | ReadonlyArray | null | undefined; export function each( arrayLikeObject: ArrayLike, callback: (value: T, indexOrKey: number, source: ArrayLike) => boolean | void ): ArrayLike; export function each( - arrayLikeObject: ArrayLike | null, + arrayLikeObject: ArrayLike | null | undefined, callback: (value: T, indexOrKey: number, source: ArrayLike) => boolean | void -): ArrayLike | null; +): ArrayLike | null | undefined; export function each(obj: PlainObject, callback: (value: any, indexOrKey: string, source: PlainObject) => boolean | void): PlainObject; -export function each(obj: PlainObject | null, callback: (value: any, indexOrKey: string, source: PlainObject) => boolean | void): PlainObject | null; +export function each( + obj: PlainObject | null | undefined, + callback: (value: any, indexOrKey: string, source: PlainObject) => boolean | void +): PlainObject | null | undefined; export function each( - source: ArrayLike | PlainObject | null, + source: ArrayLike | PlainObject | null | undefined, callback: (value: T, indexOrKey: any, source: any) => boolean | void -): Array | ReadonlyArray | ArrayLike | PlainObject | null { +): Array | ReadonlyArray | ArrayLike | PlainObject | null | undefined { if (isArrayLike(source)) { for (let i = 0; i < source.length; i++) { if (callback(source[i], i, source) === false) { @@ -67,14 +70,21 @@ export const from = (arr: ArrayLike) => { return result; }; +/** + * Check whether the passed array is empty. + * @param array The array which shall be checked. + */ +export const isEmptyArray = (array: Array | null | undefined) => array && array.length === 0; + /** * Calls all functions in the passed array/set of functions. * @param arr The array filled with function which shall be called. + * @param p1 The first param. */ -export const runEach = (arr: ArrayLike | Set): void => { +export const runEach = (arr: ArrayLike | Set, p1?: unknown): void => { if (arr instanceof Set) { - arr.forEach((fn) => fn && fn()); + arr.forEach((fn) => fn && fn(p1)); } else { - each(arr, (fn) => fn && fn()); + each(arr, (fn) => fn && fn(p1)); } }; diff --git a/packages/overlayscrollbars/src/support/utils/index.ts b/packages/overlayscrollbars/src/support/utils/index.ts index e80b708..787f33e 100644 --- a/packages/overlayscrollbars/src/support/utils/index.ts +++ b/packages/overlayscrollbars/src/support/utils/index.ts @@ -1,4 +1,5 @@ export * from 'support/utils/array'; export * from 'support/utils/equal'; +export * from 'support/utils/lexicon'; export * from 'support/utils/object'; export * from 'support/utils/types'; diff --git a/packages/overlayscrollbars/src/support/utils/lexicon.ts b/packages/overlayscrollbars/src/support/utils/lexicon.ts new file mode 100644 index 0000000..27cefbc --- /dev/null +++ b/packages/overlayscrollbars/src/support/utils/lexicon.ts @@ -0,0 +1,28 @@ +interface GenericLexicon { + _widthHeight: T extends true ? 'width' : 'height'; + _WidthHeight: T extends true ? 'Width' : 'Height'; + _leftTop: T extends true ? 'left' : 'top'; + _LeftTop: T extends true ? 'Left' : 'Top'; + _xy: T extends true ? 'x' : 'y'; + _XY: T extends true ? 'X' : 'Y'; + _wh: T extends true ? 'w' : 'h'; + _lt: T extends true ? 'l' : 't'; +} + +export interface Lexicon extends GenericLexicon { + _inverted: Lexicon; +} + +export const getLexicon = (horizontal?: T): Lexicon => { + return { + _widthHeight: horizontal ? 'width' : 'height', + _WidthHeight: horizontal ? 'Width' : 'Height', + _leftTop: horizontal ? 'left' : 'top', + _LeftTop: horizontal ? 'Left' : 'Top', + _xy: horizontal ? 'x' : 'y', + _XY: horizontal ? 'X' : 'Y', + _wh: horizontal ? 'w' : 'h', + _lt: horizontal ? 'l' : 't', + _inverted: getLexicon(!horizontal), + } as Lexicon; +}; diff --git a/packages/overlayscrollbars/tests/jsdom/support/autoUpdateLoop/autoUpdateLoop.test.ts b/packages/overlayscrollbars/tests/jsdom/support/autoUpdateLoop/autoUpdateLoop.test.ts new file mode 100644 index 0000000..0828654 --- /dev/null +++ b/packages/overlayscrollbars/tests/jsdom/support/autoUpdateLoop/autoUpdateLoop.test.ts @@ -0,0 +1,77 @@ +import { getAutoUpdateLoop } from 'autoUpdateLoop'; +import { getEnvironment } from 'environment'; + +describe('autoUpdateLoop', () => { + test('first creation', async () => { + const deltas: number[] = []; + const wait = 2700; + const loop = getAutoUpdateLoop(); + const defaultInterval = loop._interval(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const added = Date.now(); + const remove = loop._add((delta) => { + if (deltas.length === 0) { + expect(Date.now() - added >= defaultInterval).toBe(true); + } + expect(delta >= defaultInterval).toBe(true); + deltas.push(delta); + }); + expect(getEnvironment()._autoUpdateLoop).toBe(true); + + await new Promise((resolve) => setTimeout(resolve, wait)); + const elapsedDeltas = deltas.reduce((a, b) => a + b, 0); + + expect(wait - elapsedDeltas < defaultInterval * 2).toBe(true); + + remove(); + expect(getEnvironment()._autoUpdateLoop).toBe(false); + }); + + test('add multiple', async () => { + const loop = getAutoUpdateLoop(); + const fn1 = jest.fn(); + const fn2 = jest.fn(); + const fn3 = jest.fn(); + + const remove1 = loop._add(fn1); + const remove2 = loop._add(fn2); + const remove3 = loop._add(fn3); + + expect(getEnvironment()._autoUpdateLoop).toBe(true); + + await new Promise((resolve) => setTimeout(resolve, 2500)); + + expect(fn1).toHaveBeenCalledTimes(fn1.mock.calls.length); + expect(fn2).toHaveBeenCalledTimes(fn1.mock.calls.length); + expect(fn3).toHaveBeenCalledTimes(fn1.mock.calls.length); + + remove1(); + remove2(); + remove3(); + + expect(getEnvironment()._autoUpdateLoop).toBe(false); + }); + + test('change interval', async () => { + const loop = getAutoUpdateLoop(); + const defaultInterval = loop._interval(); + + const remove10 = loop._interval(10); + const remove5 = loop._interval(5); + const remove3 = loop._interval(3); + const remove8 = loop._interval(8); + const remove15 = loop._interval(15); + + expect(loop._interval()).toBe(3); + remove3(); + expect(loop._interval()).toBe(5); + remove10(); + remove8(); + expect(loop._interval()).toBe(5); + remove5(); + expect(loop._interval()).toBe(15); + remove15(); + + expect(loop._interval()).toBe(defaultInterval); + }); +}); diff --git a/packages/overlayscrollbars/tests/jsdom/support/dom/dimensions.test.ts b/packages/overlayscrollbars/tests/jsdom/support/dom/dimensions.test.ts index 7871a74..0786b36 100644 --- a/packages/overlayscrollbars/tests/jsdom/support/dom/dimensions.test.ts +++ b/packages/overlayscrollbars/tests/jsdom/support/dom/dimensions.test.ts @@ -1,6 +1,6 @@ import { isNumber, isPlainObject } from 'support/utils/types'; import { createDiv } from 'support/dom/create'; -import { windowSize, offsetSize, clientSize, getBoundingClientRect, hasDimensions } from 'support/dom/dimensions'; +import { windowSize, offsetSize, clientSize, scrollSize, getBoundingClientRect, hasDimensions } from 'support/dom/dimensions'; describe('dom dimensions', () => { describe('offsetSize', () => { @@ -35,6 +35,22 @@ describe('dom dimensions', () => { }); }); + describe('scrollSize', () => { + test('DOM element', () => { + const result = scrollSize(document.body); + expect(isPlainObject(result)).toBe(true); + expect(isNumber(result.w)).toBe(true); + expect(isNumber(result.h)).toBe(true); + }); + + test('null', () => { + const result = scrollSize(null); + expect(isPlainObject(result)).toBe(true); + expect(isNumber(result.w)).toBe(true); + expect(isNumber(result.h)).toBe(true); + }); + }); + test('windowSize', () => { const result = windowSize(); expect(isPlainObject(result)).toBe(true); diff --git a/packages/overlayscrollbars/tests/jsdom/support/dom/traversal.test.ts b/packages/overlayscrollbars/tests/jsdom/support/dom/traversal.test.ts index 2062c33..e5da87f 100644 --- a/packages/overlayscrollbars/tests/jsdom/support/dom/traversal.test.ts +++ b/packages/overlayscrollbars/tests/jsdom/support/dom/traversal.test.ts @@ -1,4 +1,4 @@ -import { find, findFirst, is, children, contents, parent, createDiv } from 'support/dom'; +import { find, findFirst, is, children, contents, parent, createDiv, liesBetween } from 'support/dom'; const slotElm = document.body; const testHTML = '

2

abc'; @@ -199,4 +199,92 @@ describe('dom traversal', () => { expect(p).toBeNull(); }); }); + + describe('liesBetween', () => { + const elmsBetween = ['.host', '.something', '.something-a', '.something-b', '.padding', '.viewport', '.content']; + const elmsOutside = ['.allowed-a', '.allowed-b', '.allowed-c', '.deeper-a', '.deeper-b']; + const elmsToTest = [...elmsBetween, ...elmsOutside]; + const domPart = (id: string, content?: string) => ` +
+
+
+
+
+
+
+
+
+
+
+
${content || ''}
+
+
+
+
+ `; + const createTestDOM = (nestings = 0) => { + let part = ''; + for (let i = 0; i < nestings + 1; i++) { + part = domPart(`host-${nestings - i}`, part); + } + + return part; + }; + const genericTest = (nestings: number) => { + const allHostIds = Array(nestings) + .fill(0) + .map((_, index) => `#host-${index}`); + + const test = (hostId: string, remainingIds: string[]) => { + const runExpectance = (id: string) => { + const isRemainingId = remainingIds.includes(id); + elmsToTest.forEach((elm) => { + const hostElm = findFirst(`${id}`); + const searchElm = hostElm?.classList.contains(elm.substring(1)) ? hostElm : findFirst(`${id} ${elm}`); + + expect(liesBetween(searchElm, `${hostId}`, '.content')).toBe( + isRemainingId ? false : elmsBetween.includes(elm) ? true : elmsOutside.includes(elm) ? false : undefined + ); + }); + }; + + runExpectance(hostId); + + remainingIds.forEach((id) => { + runExpectance(id); + }); + }; + + elmsBetween.forEach((elm) => { + expect(liesBetween(findFirst(elm), '.host', '.content')).toBe(true); + }); + + elmsOutside.forEach((elm) => { + expect(liesBetween(findFirst(elm), '.host', '.content')).toBe(false); + }); + + for (let i = 0; i < nestings; i++) { + const currHostId = allHostIds[i]; + const remainingIds = allHostIds.filter((id) => id !== currHostId); + + test(currHostId, remainingIds); + } + }; + + test('with native closest', () => { + slotElm.innerHTML = createTestDOM(3); + genericTest(3); + }); + + test('with polyfill closest', () => { + const original = Element.prototype.closest; + // @ts-ignore + Element.prototype.closest = undefined; + + slotElm.innerHTML = createTestDOM(3); + genericTest(3); + + Element.prototype.closest = original; + }); + }); });