diff --git a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.calculations.ts b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.calculations.ts index 38a602f..583b183 100644 --- a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.calculations.ts +++ b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.calculations.ts @@ -1,27 +1,39 @@ +import { offsetSize } from 'support'; import type { StructureSetupState } from 'setups'; const { min, max } = Math; export const getScrollbarHandleLengthRatio = ( - structureSetupState: StructureSetupState, - isHorizontal?: boolean + scrollbarHandle: HTMLElement, + scrollbarTrack: HTMLElement, + isHorizontal?: boolean, + structureSetupState?: StructureSetupState ) => { - const { _overflowAmount, _overflowEdge } = structureSetupState; - const axis = isHorizontal ? 'x' : 'y'; - const viewportSize = _overflowEdge[axis]; - const overflowAmount = _overflowAmount[axis]; - return max(0, min(1, viewportSize / (viewportSize + overflowAmount))); + if (structureSetupState) { + const axis = isHorizontal ? 'x' : 'y'; + const { _overflowAmount, _overflowEdge } = structureSetupState; + + const viewportSize = _overflowEdge[axis]; + const overflowAmount = _overflowAmount[axis]; + return max(0, min(1, viewportSize / (viewportSize + overflowAmount))); + } + const axis = isHorizontal ? 'w' : 'h'; + const handleSize = offsetSize(scrollbarHandle)[axis]; + const trackSize = offsetSize(scrollbarTrack)[axis]; + return max(0, min(1, handleSize / trackSize)); }; export const getScrollbarHandleOffsetRatio = ( - structureSetupState: StructureSetupState, + scrollbarHandle: HTMLElement, + scrollbarTrack: HTMLElement, scrollOffsetElement: HTMLElement, + structureSetupState: StructureSetupState, isHorizontal?: boolean ) => { const axis = isHorizontal ? 'x' : 'y'; const scrollLeftTop = isHorizontal ? 'Left' : 'Top'; - const lengthRatio = getScrollbarHandleLengthRatio(structureSetupState, isHorizontal); + const { _overflowAmount } = structureSetupState; const scrollPosition = scrollOffsetElement[`scroll${scrollLeftTop}`] as number; - const scrollPositionMax = Math.floor(structureSetupState._overflowAmount[axis]); + const scrollPositionMax = Math.floor(_overflowAmount[axis]); const scrollPercent = min(1, scrollPosition / scrollPositionMax); - + const lengthRatio = getScrollbarHandleLengthRatio(scrollbarHandle, scrollbarTrack, isHorizontal); return (1 / lengthRatio) * (1 - lengthRatio) * scrollPercent; }; diff --git a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.elements.ts b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.elements.ts index 0f0e905..a040948 100644 --- a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.elements.ts +++ b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.elements.ts @@ -22,6 +22,10 @@ import { } from 'classnames'; import { getEnvironment } from 'environment'; import { dynamicInitializationElement as generalDynamicInitializationElement } from 'initialization'; +import { + getScrollbarHandleLengthRatio, + getScrollbarHandleOffsetRatio, +} from 'setups/scrollbarsSetup/scrollbarsSetup.calculations'; import type { InitializationTarget } from 'initialization'; import type { StructureSetupElementsObj } from 'setups/structureSetup/structureSetup.elements'; import type { ScrollbarsSetupEvents } from 'setups/scrollbarsSetup/scrollbarsSetup.events'; @@ -30,6 +34,7 @@ import type { ScrollbarsDynamicInitializationElement, } from 'setups/scrollbarsSetup/scrollbarsSetup.initialization'; import type { StyleObject } from 'typings'; +import { StructureSetupState } from 'setups'; export interface ScrollbarStructure { _scrollbar: HTMLElement; @@ -53,6 +58,8 @@ export interface ScrollbarsSetupElementsObj { add?: boolean, isHorizontal?: boolean ) => void; + _refreshScrollbarsHandleLength: (structureSetupState: StructureSetupState) => void; + _refreshScrollbarsHandleOffset: (structureSetupState: StructureSetupState) => void; _horizontal: ScrollbarsSetupElement; _vertical: ScrollbarsSetupElement; } @@ -101,6 +108,51 @@ export const createScrollbarsSetupElements = ( style(elm, styles); }); }; + const scrollbarStructureRefreshHandleLength = ( + scrollbarStructures: ScrollbarStructure[], + structureSetupState: StructureSetupState, + isHorizontal?: boolean + ) => { + scrollbarsHandleStyle(scrollbarStructures, (structure) => { + const { _handle, _track } = structure; + return [ + _handle, + { + [isHorizontal ? 'width' : 'height']: `${( + getScrollbarHandleLengthRatio(_handle, _track, isHorizontal, structureSetupState) * 100 + ).toFixed(3)}%`, + }, + ]; + }); + }; + const scrollbarStructureRefreshHandleOffset = ( + scrollbarStructures: ScrollbarStructure[], + structureSetupState: StructureSetupState, + isHorizontal?: boolean + ) => { + const translateAxis = isHorizontal ? 'X' : 'Y'; + scrollbarsHandleStyle(scrollbarStructures, (structure) => { + const { _handle, _track } = structure; + const offsetRatio = getScrollbarHandleOffsetRatio( + _handle, + _track, + _scrollOffsetElement, + structureSetupState, + isHorizontal + ); + // eslint-disable-next-line no-self-compare + const validOffsetRatio = offsetRatio === offsetRatio; // is false when offset is NaN + return [ + _handle, + { + transform: validOffsetRatio + ? `translate${translateAxis}(${(offsetRatio * 100).toFixed(3)}%)` + : '', + }, + ]; + }); + }; + const destroyFns: (() => void)[] = []; const horizontalScrollbars: ScrollbarStructure[] = []; const verticalScrollbars: ScrollbarStructure[] = []; @@ -116,6 +168,14 @@ export const createScrollbarsSetupElements = ( runHorizontal && scrollbarStructureAddRemoveClass(horizontalScrollbars, className, add); runVertical && scrollbarStructureAddRemoveClass(verticalScrollbars, className, add); }; + const refreshScrollbarsHandleLength = (structureSetupState: StructureSetupState) => { + scrollbarStructureRefreshHandleLength(horizontalScrollbars, structureSetupState, true); + scrollbarStructureRefreshHandleLength(verticalScrollbars, structureSetupState); + }; + const refreshScrollbarsHandleOffset = (structureSetupState: StructureSetupState) => { + scrollbarStructureRefreshHandleOffset(horizontalScrollbars, structureSetupState, true); + scrollbarStructureRefreshHandleOffset(verticalScrollbars, structureSetupState); + }; const generateScrollbarDOM = (isHorizontal?: boolean): ScrollbarStructure => { const scrollbarClassName = isHorizontal ? classNameScrollbarHorizontal @@ -166,6 +226,8 @@ export const createScrollbarsSetupElements = ( return [ { + _refreshScrollbarsHandleLength: refreshScrollbarsHandleLength, + _refreshScrollbarsHandleOffset: refreshScrollbarsHandleOffset, _scrollbarsAddRemoveClass: scrollbarsAddRemoveClass, _horizontal: { _scrollbarStructures: horizontalScrollbars, diff --git a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.events.ts b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.events.ts index 3efb1df..73a768d 100644 --- a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.events.ts +++ b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.events.ts @@ -8,7 +8,6 @@ import { XY, } from 'support'; import { classNamesScrollbarInteraction } from 'classnames'; -import { getScrollbarHandleLengthRatio } from 'setups/scrollbarsSetup/scrollbarsSetup.calculations'; import type { ReadonlyOptions } from 'options'; import type { StructureSetupState } from 'setups'; import type { @@ -36,12 +35,21 @@ const getScale = (element: HTMLElement): XY => { y: Math.round(height) / h || 1, }; }; -const continuePointerDown = (event: PointerEvent, options: ReadonlyOptions, scrollType: 'dragScroll' | 'clickScroll') => { +const continuePointerDown = ( + event: PointerEvent, + options: ReadonlyOptions, + scrollType: 'dragScroll' | 'clickScroll' +) => { const scrollbarOptions = options.scrollbars; const { button, isPrimary, pointerType } = event; const { pointers } = scrollbarOptions; - return button === 0 && isPrimary && scrollbarOptions[scrollType] && (pointers || []).includes(pointerType); -} + return ( + button === 0 && + isPrimary && + scrollbarOptions[scrollType] && + (pointers || []).includes(pointerType) + ); +}; const createRootClickStopPropagationEvents = (scrollbar: HTMLElement, documentElm: Document) => on( scrollbar, @@ -52,30 +60,35 @@ const createRootClickStopPropagationEvents = (scrollbar: HTMLElement, documentEl const createDragScrollingEvents = ( options: ReadonlyOptions, doc: Document, - scrollbarHandle: HTMLElement, + scrollbarStructure: ScrollbarStructure, scrollOffsetElement: HTMLElement, structureSetupState: () => StructureSetupState, isHorizontal?: boolean ) => { + const { _handle, _track } = scrollbarStructure; const scrollOffsetKey = `scroll${isHorizontal ? 'Left' : 'Top'}`; const xyKey = `${isHorizontal ? 'x' : 'y'}`; + const whKey = `${isHorizontal ? 'w' : 'h'}`; const createOnPointerMoveHandler = (mouseDownScroll: number, mouseDownPageOffset: number, mouseDownInvertedScale: number) => (event: PointerEvent) => { + const { _overflowAmount } = structureSetupState(); const movement = (getPageOffset(event)[xyKey] - mouseDownPageOffset) * mouseDownInvertedScale; - const handleLengthRatio = - 1 / getScrollbarHandleLengthRatio(structureSetupState(), isHorizontal); - scrollOffsetElement[scrollOffsetKey] = mouseDownScroll + movement * handleLengthRatio; + const handleTrackDiff = offsetSize(_track)[whKey] - offsetSize(_handle)[whKey]; + const scrollDeltaPercent = movement / handleTrackDiff; + const scrollDelta = scrollDeltaPercent * _overflowAmount[xyKey]; + + scrollOffsetElement[scrollOffsetKey] = mouseDownScroll + scrollDelta; // if (_isRTL && isHorizontal && !_rtlScrollBehavior.i) scrollDelta *= -1; }; - return on(scrollbarHandle, 'pointerdown', (pointerDownEvent: PointerEvent) => { + return on(_handle, 'pointerdown', (pointerDownEvent: PointerEvent) => { if (continuePointerDown(pointerDownEvent, options, 'dragScroll')) { const offSelectStart = on(doc, 'selectstart', (event: Event) => preventDefault(event), { _passive: false, }); const offPointerMove = on( - scrollbarHandle, + _handle, 'pointermove', createOnPointerMoveHandler( scrollOffsetElement[scrollOffsetKey] || 0, @@ -85,24 +98,27 @@ const createDragScrollingEvents = ( ); on( - scrollbarHandle, + _handle, 'pointerup', (pointerUpEvent: PointerEvent) => { offSelectStart(); offPointerMove(); - scrollbarHandle.releasePointerCapture(pointerUpEvent.pointerId); + _handle.releasePointerCapture(pointerUpEvent.pointerId); }, { _once: true } ); - scrollbarHandle.setPointerCapture(pointerDownEvent.pointerId); + _handle.setPointerCapture(pointerDownEvent.pointerId); } }); }; export const createScrollbarsSetupEvents = - (options: ReadonlyOptions, structureSetupState: () => StructureSetupState): ScrollbarsSetupEvents => + ( + options: ReadonlyOptions, + structureSetupState: () => StructureSetupState + ): ScrollbarsSetupEvents => (scrollbarStructure, scrollbarsAddRemoveClass, documentElm, scrollOffsetElm, isHorizontal) => { - const { _scrollbar, _handle } = scrollbarStructure; + const { _scrollbar } = scrollbarStructure; return runEachAndClear.bind(0, [ on(_scrollbar, 'pointerenter', () => { @@ -115,7 +131,7 @@ export const createScrollbarsSetupEvents = createDragScrollingEvents( options, documentElm, - _handle, + scrollbarStructure, scrollOffsetElm, structureSetupState, isHorizontal diff --git a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts index 4860428..582f6c5 100644 --- a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts +++ b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts @@ -11,14 +11,9 @@ import { scrollTop, } from 'support'; import { createState, createOptionCheck } from 'setups/setups'; -import { - getScrollbarHandleLengthRatio, - getScrollbarHandleOffsetRatio, -} from 'setups/scrollbarsSetup/scrollbarsSetup.calculations'; import { createScrollbarsSetupEvents } from 'setups/scrollbarsSetup/scrollbarsSetup.events'; import { createScrollbarsSetupElements, - ScrollbarsSetupElement, ScrollbarsSetupElementsObj, ScrollbarStructure, } from 'setups/scrollbarsSetup/scrollbarsSetup.elements'; @@ -62,45 +57,6 @@ const createSelfCancelTimeout = (timeout?: number | (() => number)) => { ] as [timeout: (callback: () => any) => void, clear: () => void]; }; -const refreshScrollbarHandleLength = ( - setStyleFn: ScrollbarsSetupElement['_handleStyle'], - structureSetupState: StructureSetupState, - isHorizontal?: boolean -) => - setStyleFn((structure) => [ - structure._handle, - { - [isHorizontal ? 'width' : 'height']: `${( - getScrollbarHandleLengthRatio(structureSetupState, isHorizontal) * 100 - ).toFixed(3)}%`, - }, - ]); - -const refreshScrollbarHandleOffset = ( - setStyleFn: ScrollbarsSetupElement['_handleStyle'], - structureSetupState: StructureSetupState, - scrollOffsetElement: HTMLElement, - isHorizontal?: boolean -) => { - const translateAxis = isHorizontal ? 'X' : 'Y'; - const offsetRatio = getScrollbarHandleOffsetRatio( - structureSetupState, - scrollOffsetElement, - isHorizontal - ); - // eslint-disable-next-line no-self-compare - const validOffsetRatio = offsetRatio === offsetRatio; // is false when offset is NaN - - setStyleFn((structure) => [ - structure._handle, - { - transform: validOffsetRatio - ? `translate${translateAxis}(${(offsetRatio * 100).toFixed(3)}%)` - : '', - }, - ]); -}; - export const createScrollbarsSetup = ( target: InitializationTarget, options: ReadonlyOptions, @@ -133,7 +89,13 @@ export const createScrollbarsSetup = ( _viewportIsTarget, _isBody, } = structureSetupState._elements; - const { _horizontal, _vertical, _scrollbarsAddRemoveClass: scrollbarsAddRemoveClass } = elements; + const { + _horizontal, + _vertical, + _scrollbarsAddRemoveClass: scrollbarsAddRemoveClass, + _refreshScrollbarsHandleLength, + _refreshScrollbarsHandleOffset, + } = elements; const { _handleStyle: styleHorizontal } = _horizontal; const { _handleStyle: styleVertical } = _vertical; const styleScrollbarPosition = (structure: ScrollbarStructure) => { @@ -192,9 +154,7 @@ export const createScrollbarsSetup = ( }), on(_scrollEventElement, 'scroll', () => { requestScrollAnimationFrame(() => { - const structureState = structureSetupState(); - refreshScrollbarHandleOffset(styleHorizontal, structureState, _scrollOffsetElement, true); - refreshScrollbarHandleOffset(styleVertical, structureState, _scrollOffsetElement); + _refreshScrollbarsHandleOffset(structureSetupState()); autoHideNotNever && manageScrollbarsAutoHide(true); scrollTimeout(() => { @@ -217,7 +177,7 @@ export const createScrollbarsSetup = ( structureUpdateHints; const checkOption = createOptionCheck(options, changedOptions, force); const currStructureSetupState = structureSetupState(); - + const { _overflowAmount, _overflowStyle } = currStructureSetupState; const [theme, themeChanged] = checkOption('scrollbars.theme'); const [visibility, visibilityChanged] = checkOption('scrollbars.visibility'); @@ -258,8 +218,6 @@ export const createScrollbarsSetup = ( scrollbarsAddRemoveClass(classNamesScrollbarTrackInteractive, clickScroll); } if (updateVisibility) { - const { _overflowStyle } = currStructureSetupState; - const xVisible = setScrollbarVisibility(_overflowStyle.x, true); const yVisible = setScrollbarVisibility(_overflowStyle.y, false); const hasCorner = xVisible && yVisible; @@ -267,17 +225,8 @@ export const createScrollbarsSetup = ( scrollbarsAddRemoveClass(classNamesScrollbarCornerless, !hasCorner); } if (updateHandle) { - const { _overflowAmount } = currStructureSetupState; - refreshScrollbarHandleLength(styleHorizontal, currStructureSetupState, true); - refreshScrollbarHandleLength(styleVertical, currStructureSetupState); - - refreshScrollbarHandleOffset( - styleHorizontal, - currStructureSetupState, - _scrollOffsetElement, - true - ); - refreshScrollbarHandleOffset(styleVertical, currStructureSetupState, _scrollOffsetElement); + _refreshScrollbarsHandleLength(currStructureSetupState); + _refreshScrollbarsHandleOffset(currStructureSetupState); scrollbarsAddRemoveClass(classNamesScrollbarUnusable, !_overflowAmount.x, true); scrollbarsAddRemoveClass(classNamesScrollbarUnusable, !_overflowAmount.y, false);