add drag scrolling & improve code

This commit is contained in:
Rene Haas
2022-07-27 01:11:05 +02:00
parent 78cef932fd
commit 4d4532f74d
6 changed files with 233 additions and 110 deletions
@@ -0,0 +1,27 @@
import type { StructureSetupState } from 'setups';
const { min, max } = Math;
export const getScrollbarHandleLengthRatio = (
structureSetupState: StructureSetupState,
isHorizontal?: boolean
) => {
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)));
};
export const getScrollbarHandleOffsetRatio = (
structureSetupState: StructureSetupState,
scrollOffsetElement: HTMLElement,
isHorizontal?: boolean
) => {
const axis = isHorizontal ? 'x' : 'y';
const scrollLeftTop = isHorizontal ? 'Left' : 'Top';
const lengthRatio = getScrollbarHandleLengthRatio(structureSetupState, isHorizontal);
const scrollPosition = scrollOffsetElement[`scroll${scrollLeftTop}`] as number;
const scrollPositionMax = Math.floor(structureSetupState._overflowAmount[axis]);
const scrollPercent = min(1, scrollPosition / scrollPositionMax);
return (1 / lengthRatio) * (1 - lengthRatio) * scrollPercent;
};
@@ -3,15 +3,13 @@ import {
appendChildren,
createDiv,
each,
isBoolean,
isEmptyArray,
noop,
on,
push,
removeClass,
removeElements,
runEachAndClear,
setT,
stopPropagation,
style,
} from 'support';
import {
@@ -20,18 +18,18 @@ import {
classNameScrollbarVertical,
classNameScrollbarTrack,
classNameScrollbarHandle,
classNamesScrollbarInteraction,
classNamesScrollbarTransitionless,
} from 'classnames';
import { getEnvironment } from 'environment';
import { dynamicInitializationElement as generalDynamicInitializationElement } from 'initialization';
import type { InitializationTarget } from 'initialization';
import type { StructureSetupElementsObj } from 'setups/structureSetup/structureSetup.elements';
import type { ScrollbarsSetupEvents } from 'setups/scrollbarsSetup/scrollbarsSetup.events';
import type {
ScrollbarsInitialization,
ScrollbarsDynamicInitializationElement,
} from 'setups/scrollbarsSetup/scrollbarsSetup.initialization';
import { StyleObject } from 'typings';
import type { StyleObject } from 'typings';
export interface ScrollbarStructure {
_scrollbar: HTMLElement;
@@ -42,24 +40,19 @@ export interface ScrollbarStructure {
export interface ScrollbarsSetupElement {
_scrollbarStructures: ScrollbarStructure[];
_clone: () => ScrollbarStructure;
_addRemoveClass: (
classNames: string | false | null | undefined,
add?: boolean,
elm?: (scrollbarStructure: ScrollbarStructure) => HTMLElement | false | null | undefined
) => void;
_handleStyle: (
elmStyle: (
scrollbarStructure: ScrollbarStructure
) => [HTMLElement | false | null | undefined, StyleObject]
) => void;
// _removeClass: (classNames: string) => void;
/*
_addEventListener: () => void;
_removeEventListener: () => void;
*/
}
export interface ScrollbarsSetupElementsObj {
_scrollbarsAddRemoveClass: (
classNames: string | false | null | undefined,
add?: boolean,
isHorizontal?: boolean
) => void;
_horizontal: ScrollbarsSetupElement;
_vertical: ScrollbarsSetupElement;
}
@@ -70,23 +63,15 @@ export type ScrollbarsSetupElements = [
destroy: () => void
];
const interactionStartEventNames = 'touchstart mouseenter';
const interactionEndEventNames = 'touchend touchcancel mouseleave';
const stopRootClickPropagation = (scrollbar: HTMLElement, documentElm: Document) =>
on(
scrollbar,
'mousedown',
on.bind(0, documentElm, 'click', stopPropagation, { _once: true, _capture: true }),
{ _capture: true }
);
export const createScrollbarsSetupElements = (
target: InitializationTarget,
structureSetupElements: StructureSetupElementsObj
structureSetupElements: StructureSetupElementsObj,
scrollbarsSetupEvents: ScrollbarsSetupEvents
): ScrollbarsSetupElements => {
const { _getDefaultInitialization } = getEnvironment();
const { scrollbarsSlot: defaultScrollbarsSlot } = _getDefaultInitialization();
const { _documentElm, _target, _host, _viewport, _targetIsElm } = structureSetupElements;
const { _documentElm, _target, _host, _viewport, _targetIsElm, _scrollOffsetElement } =
structureSetupElements;
const { scrollbarsSlot } = (_targetIsElm ? {} : target) as ScrollbarsInitialization;
const evaluatedScrollbarSlot =
generalDynamicInitializationElement<ScrollbarsDynamicInitializationElement>(
@@ -95,15 +80,14 @@ export const createScrollbarsSetupElements = (
defaultScrollbarsSlot,
scrollbarsSlot
);
const scrollbarsAddRemoveClass = (
const scrollbarStructureAddRemoveClass = (
scrollbarStructures: ScrollbarStructure[],
classNames: string | false | null | undefined,
add?: boolean,
elm?: (scrollbarStructure: ScrollbarStructure) => HTMLElement | false | null | undefined
add?: boolean
) => {
const action = add ? addClass : removeClass;
each(scrollbarStructures, (scrollbarStructure) => {
action((elm || noop)(scrollbarStructure) || scrollbarStructure._scrollbar, classNames);
action(scrollbarStructure._scrollbar, classNames);
});
};
const scrollbarsHandleStyle = (
@@ -121,13 +105,22 @@ export const createScrollbarsSetupElements = (
const horizontalScrollbars: ScrollbarStructure[] = [];
const verticalScrollbars: ScrollbarStructure[] = [];
const addRemoveClassHorizontal = scrollbarsAddRemoveClass.bind(0, horizontalScrollbars);
const addRemoveClassVertical = scrollbarsAddRemoveClass.bind(0, verticalScrollbars);
const generateScrollbarDOM = (horizontal?: boolean): ScrollbarStructure => {
const scrollbarClassName = horizontal
const scrollbarsAddRemoveClass = (
className: string | false | null | undefined,
add?: boolean,
onlyHorizontal?: boolean
) => {
const singleAxis = isBoolean(onlyHorizontal);
const runHorizontal = singleAxis ? onlyHorizontal : true;
const runVertical = singleAxis ? !onlyHorizontal : true;
runHorizontal && scrollbarStructureAddRemoveClass(horizontalScrollbars, className, add);
runVertical && scrollbarStructureAddRemoveClass(verticalScrollbars, className, add);
};
const generateScrollbarDOM = (isHorizontal?: boolean): ScrollbarStructure => {
const scrollbarClassName = isHorizontal
? classNameScrollbarHorizontal
: classNameScrollbarVertical;
const arrToPush = horizontal ? horizontalScrollbars : verticalScrollbars;
const arrToPush = isHorizontal ? horizontalScrollbars : verticalScrollbars;
const transitionlessClass = isEmptyArray(arrToPush) ? classNamesScrollbarTransitionless : '';
const scrollbar = createDiv(
`${classNameScrollbar} ${scrollbarClassName} ${transitionlessClass}`
@@ -146,15 +139,13 @@ export const createScrollbarsSetupElements = (
push(arrToPush, result);
push(destroyFns, [
removeElements.bind(0, scrollbar),
on(scrollbar, interactionStartEventNames, () => {
addRemoveClassHorizontal(classNamesScrollbarInteraction, true);
addRemoveClassVertical(classNamesScrollbarInteraction, true);
}),
on(scrollbar, interactionEndEventNames, () => {
addRemoveClassHorizontal(classNamesScrollbarInteraction);
addRemoveClassVertical(classNamesScrollbarInteraction);
}),
stopRootClickPropagation(scrollbar, _documentElm),
scrollbarsSetupEvents(
result,
scrollbarsAddRemoveClass,
_documentElm,
_scrollOffsetElement,
isHorizontal
),
]);
return result;
@@ -166,8 +157,7 @@ export const createScrollbarsSetupElements = (
appendChildren(evaluatedScrollbarSlot, verticalScrollbars[0]._scrollbar);
setT(() => {
addRemoveClassHorizontal(classNamesScrollbarTransitionless);
addRemoveClassVertical(classNamesScrollbarTransitionless);
scrollbarsAddRemoveClass(classNamesScrollbarTransitionless);
}, 300);
};
@@ -176,16 +166,15 @@ export const createScrollbarsSetupElements = (
return [
{
_scrollbarsAddRemoveClass: scrollbarsAddRemoveClass,
_horizontal: {
_scrollbarStructures: horizontalScrollbars,
_clone: generateHorizontalScrollbarStructure,
_addRemoveClass: addRemoveClassHorizontal,
_handleStyle: scrollbarsHandleStyle.bind(0, horizontalScrollbars),
},
_vertical: {
_scrollbarStructures: verticalScrollbars,
_clone: generateVerticalScrollbarStructure,
_addRemoveClass: addRemoveClassVertical,
_handleStyle: scrollbarsHandleStyle.bind(0, verticalScrollbars),
},
},
@@ -0,0 +1,116 @@
import {
getBoundingClientRect,
offsetSize,
on,
preventDefault,
runEachAndClear,
stopPropagation,
XY,
} from 'support';
import { classNamesScrollbarInteraction } from 'classnames';
import { getScrollbarHandleOffsetRatio } from 'setups/scrollbarsSetup/scrollbarsSetup.calculations';
import type { StructureSetupState } from 'setups';
import type {
ScrollbarsSetupElementsObj,
ScrollbarStructure,
} from 'setups/scrollbarsSetup/scrollbarsSetup.elements';
export type ScrollbarsSetupEvents = (
scrollbarStructure: ScrollbarStructure,
scrollbarsAddRemoveClass: ScrollbarsSetupElementsObj['_scrollbarsAddRemoveClass'],
documentElm: Document,
scrollOffsetElm: HTMLElement,
isHorizontal?: boolean
) => () => void;
const getPageOffset = (event: PointerEvent): XY<number> => ({
x: event.pageX,
y: event.pageY,
});
const getInvertedScale = (element: HTMLElement): XY<number> => {
const { width, height } = getBoundingClientRect(element);
const { w, h } = offsetSize(element);
return {
x: 1 / (Math.round(width) / w) || 1,
y: 1 / (Math.round(height) / h) || 1,
};
};
const createRootClickStopPropagationEvents = (scrollbar: HTMLElement, documentElm: Document) =>
on(
scrollbar,
'mousedown',
on.bind(0, documentElm, 'click', stopPropagation, { _once: true, _capture: true }),
{ _capture: true }
);
const createDragScrollingEvents = (
doc: Document,
scrollbarHandle: HTMLElement,
scrollOffsetElement: HTMLElement,
structureSetupState: () => StructureSetupState,
isHorizontal?: boolean
) => {
const scrollOffsetKey = `scroll${isHorizontal ? 'Left' : 'Top'}`;
const xyKey = `${isHorizontal ? 'x' : 'y'}`;
const createOnPointerMoveHandler =
(mouseDownScroll: number, mouseDownPageOffset: number, mouseDownInvertedScale: number) =>
(event: PointerEvent) => {
const movement = (getPageOffset(event)[xyKey] - mouseDownPageOffset) * mouseDownInvertedScale;
const handleLengthRatio =
1 / getScrollbarHandleOffsetRatio(structureSetupState(), scrollOffsetElement, isHorizontal);
scrollOffsetElement[scrollOffsetKey] = mouseDownScroll + movement * handleLengthRatio;
// if (_isRTL && isHorizontal && !_rtlScrollBehavior.i) scrollDelta *= -1;
};
return on(scrollbarHandle, 'pointerdown', (pointerDownEvent: PointerEvent) => {
const { button, isPrimary, pointerId } = pointerDownEvent;
if (button === 0 && isPrimary) {
const mouseDownScroll = scrollOffsetElement[scrollOffsetKey] || 0;
const mouseDownPageOffset = getPageOffset(pointerDownEvent)[xyKey];
const mouseDownInvertedScale = getInvertedScale(scrollOffsetElement)[xyKey];
const offSelectStart = on(doc, 'selectstart', (event: Event) => preventDefault(event), {
_passive: false,
});
const offPointerMove = on(
scrollbarHandle,
'pointermove',
createOnPointerMoveHandler(mouseDownScroll, mouseDownPageOffset, mouseDownInvertedScale)
);
on(
scrollbarHandle,
'pointerup',
(pointerUpEvent: PointerEvent) => {
offSelectStart();
offPointerMove();
scrollbarHandle.releasePointerCapture(pointerUpEvent.pointerId);
},
{ _once: true }
);
scrollbarHandle.setPointerCapture(pointerId);
}
});
};
export const createScrollbarsSetupEvents =
(structureSetupState: () => StructureSetupState): ScrollbarsSetupEvents =>
(scrollbarStructure, scrollbarsAddRemoveClass, documentElm, scrollOffsetElm, isHorizontal) => {
const { _scrollbar, _handle } = scrollbarStructure;
return runEachAndClear.bind(0, [
on(_scrollbar, 'pointerenter', () => {
scrollbarsAddRemoveClass(classNamesScrollbarInteraction, true);
}),
on(_scrollbar, 'pointerleave pointercancel', () => {
scrollbarsAddRemoveClass(classNamesScrollbarInteraction);
}),
createRootClickStopPropagationEvents(_scrollbar, documentElm),
createDragScrollingEvents(
documentElm,
_handle,
scrollOffsetElm,
structureSetupState,
isHorizontal
),
]);
};
@@ -11,6 +11,11 @@ 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,
@@ -40,7 +45,6 @@ export interface ScrollbarsSetupStaticState {
_appendElements: () => void;
}
const { min } = Math;
const createSelfCancelTimeout = (timeout?: number | (() => number)) => {
let id: number;
const setTFn = timeout ? setT : rAF!;
@@ -55,17 +59,6 @@ const createSelfCancelTimeout = (timeout?: number | (() => number)) => {
] as [timeout: (callback: () => any) => void, clear: () => void];
};
const getScrollbarHandleRatio = (
structureSetupState: StructureSetupState,
isHorizontal?: boolean
) => {
const { _overflowAmount, _overflowEdge } = structureSetupState;
const axis = isHorizontal ? 'x' : 'y';
const viewportSize = _overflowEdge[axis];
const overflowAmount = _overflowAmount[axis];
return min(1, viewportSize / (viewportSize + overflowAmount));
};
const refreshScrollbarHandleLength = (
setStyleFn: ScrollbarsSetupElement['_handleStyle'],
structureSetupState: StructureSetupState,
@@ -75,7 +68,7 @@ const refreshScrollbarHandleLength = (
structure._handle,
{
[isHorizontal ? 'width' : 'height']: `${(
getScrollbarHandleRatio(structureSetupState, isHorizontal) * 100
getScrollbarHandleLengthRatio(structureSetupState, isHorizontal) * 100
).toFixed(3)}%`,
},
]);
@@ -83,28 +76,23 @@ const refreshScrollbarHandleLength = (
const refreshScrollbarHandleOffset = (
setStyleFn: ScrollbarsSetupElement['_handleStyle'],
structureSetupState: StructureSetupState,
viewport: HTMLElement,
scrollOffsetElement: HTMLElement,
isHorizontal?: boolean
) => {
const axis = isHorizontal ? 'x' : 'y';
const translateAxis = isHorizontal ? 'X' : 'Y';
const scrollLeftTop = isHorizontal ? 'Left' : 'Top';
const handleRatio = getScrollbarHandleRatio(structureSetupState, isHorizontal);
const scrollPosition = viewport[`scroll${scrollLeftTop}`] as number;
const scrollPositionMax =
(viewport[`scroll${scrollLeftTop}Max`] as number) ||
Math.floor(structureSetupState._overflowAmount[axis]);
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: scrollPositionMax
? `translate${translateAxis}(${(
(1 / handleRatio) *
(1 - handleRatio) *
(scrollPosition / scrollPositionMax) *
100
).toFixed(3)}%)`
transform: validOffsetRatio
? `translate${translateAxis}(${(offsetRatio * 100).toFixed(3)}%)`
: '',
},
]);
@@ -131,34 +119,38 @@ export const createScrollbarsSetup = (
const [auotHideTimeout, clearAutoTimeout] = createSelfCancelTimeout(() => globalAutoHideDelay);
const [elements, appendElements, destroyElements] = createScrollbarsSetupElements(
target,
structureSetupState._elements
structureSetupState._elements,
createScrollbarsSetupEvents(structureSetupState)
);
const { _host, _viewport, _viewportIsTarget, _isBody, _documentElm } =
structureSetupState._elements;
const scrollOffsetElement = _isBody ? _documentElm.documentElement : _viewport;
const { _horizontal, _vertical } = elements;
const { _addRemoveClass: addRemoveClassHorizontal, _handleStyle: styleHorizontal } = _horizontal;
const { _addRemoveClass: addRemoveClassVertical, _handleStyle: styleVertical } = _vertical;
const {
_host,
_viewport,
_scrollOffsetElement,
_scrollEventElement,
_viewportIsTarget,
_isBody,
} = structureSetupState._elements;
const { _horizontal, _vertical, _scrollbarsAddRemoveClass: scrollbarsAddRemoveClass } = elements;
const { _handleStyle: styleHorizontal } = _horizontal;
const { _handleStyle: styleVertical } = _vertical;
const styleScrollbarPosition = (structure: ScrollbarStructure) => {
const { _scrollbar } = structure;
const elm = _viewportIsTarget && !_isBody && parent(_scrollbar) === _viewport && _scrollbar;
return [
elm,
{
transform: elm ? `translate(${scrollLeft(_viewport)}px, ${scrollTop(_viewport)}px)` : '',
transform: elm
? `translate(${scrollLeft(_scrollOffsetElement)}px, ${scrollTop(_scrollOffsetElement)}px)`
: '',
},
] as [HTMLElement | false, StyleObject];
};
const manageScrollbarsAutoHide = (removeAutoHide: boolean, delayless?: boolean) => {
clearAutoTimeout();
if (removeAutoHide) {
addRemoveClassHorizontal(classNamesScrollbarAutoHidden);
addRemoveClassVertical(classNamesScrollbarAutoHidden);
scrollbarsAddRemoveClass(classNamesScrollbarAutoHidden);
} else {
const hide = () => {
addRemoveClassHorizontal(classNamesScrollbarAutoHidden, true);
addRemoveClassVertical(classNamesScrollbarAutoHidden, true);
};
const hide = () => scrollbarsAddRemoveClass(classNamesScrollbarAutoHidden, true);
if (globalAutoHideDelay > 0 && !delayless) {
auotHideTimeout(hide);
} else {
@@ -195,11 +187,11 @@ export const createScrollbarsSetup = (
});
});
}),
on(_isBody ? _documentElm : _viewport, 'scroll', () => {
on(_scrollEventElement, 'scroll', () => {
requestScrollAnimationFrame(() => {
const structureState = structureSetupState();
refreshScrollbarHandleOffset(styleHorizontal, structureState, scrollOffsetElement, true);
refreshScrollbarHandleOffset(styleVertical, structureState, scrollOffsetElement);
refreshScrollbarHandleOffset(styleHorizontal, structureState, _scrollOffsetElement, true);
refreshScrollbarHandleOffset(styleVertical, structureState, _scrollOffsetElement);
autoHideNotNever && manageScrollbarsAutoHide(true);
scrollTimeout(() => {
@@ -237,13 +229,10 @@ export const createScrollbarsSetup = (
const updateHandle = _overflowEdgeChanged || _overflowAmountChanged;
const updateVisibility = _overflowStyleChanged || visibilityChanged;
const setScrollbarVisibility = (
overflowStyle: OverflowStyle,
addRemoveClass: (classNames: string, add?: boolean) => void
) => {
const setScrollbarVisibility = (overflowStyle: OverflowStyle, isHorizontal: boolean) => {
const isVisible =
visibility === 'visible' || (visibility === 'auto' && overflowStyle === 'scroll');
addRemoveClass(classNamesScrollbarVisible, isVisible);
scrollbarsAddRemoveClass(classNamesScrollbarVisible, isVisible, isHorizontal);
return isVisible;
};
@@ -252,19 +241,16 @@ export const createScrollbarsSetup = (
if (updateVisibility) {
const { _overflowStyle } = currStructureSetupState;
const xVisible = setScrollbarVisibility(_overflowStyle.x, addRemoveClassHorizontal);
const yVisible = setScrollbarVisibility(_overflowStyle.y, addRemoveClassVertical);
const xVisible = setScrollbarVisibility(_overflowStyle.x, true);
const yVisible = setScrollbarVisibility(_overflowStyle.y, false);
const hasCorner = xVisible && yVisible;
addRemoveClassHorizontal(classNamesScrollbarCornerless, !hasCorner);
addRemoveClassVertical(classNamesScrollbarCornerless, !hasCorner);
scrollbarsAddRemoveClass(classNamesScrollbarCornerless, !hasCorner);
}
if (themeChanged) {
addRemoveClassHorizontal(prevTheme);
addRemoveClassVertical(prevTheme);
scrollbarsAddRemoveClass(prevTheme);
scrollbarsAddRemoveClass(theme, true);
addRemoveClassHorizontal(theme, true);
addRemoveClassVertical(theme, true);
prevTheme = theme;
}
if (autoHideChanged) {
@@ -280,10 +266,10 @@ export const createScrollbarsSetup = (
refreshScrollbarHandleOffset(
styleHorizontal,
currStructureSetupState,
scrollOffsetElement,
_scrollOffsetElement,
true
);
refreshScrollbarHandleOffset(styleVertical, currStructureSetupState, scrollOffsetElement);
refreshScrollbarHandleOffset(styleVertical, currStructureSetupState, _scrollOffsetElement);
}
},
scrollbarsSetupState,
@@ -60,6 +60,8 @@ export interface StructureSetupElementsObj {
_padding: HTMLElement | false;
_content: HTMLElement | false;
_viewportArrange: HTMLStyleElement | false | null | undefined;
_scrollOffsetElement: HTMLElement;
_scrollEventElement: HTMLElement | Document;
// ctx ----
_isTextarea: boolean;
_isBody: boolean;
@@ -152,6 +154,8 @@ export const createStructureSetupElements = (
!_nativeScrollbarsHiding &&
createUniqueViewportArrangeElement &&
createUniqueViewportArrangeElement(env),
_scrollOffsetElement: isBody ? ownerDocument.documentElement : viewportElement,
_scrollEventElement: isBody ? ownerDocument : viewportElement,
_windowElm: wnd,
_documentElm: ownerDocument,
_isTextarea: isTextarea,
@@ -25,7 +25,7 @@ body > .os-scrollbar {
border: none !important;
}
.os-scrollbar-handle {
pointer-events: none;
pointer-events: auto;
position: absolute;
width: 100%;
height: 100%;
@@ -33,6 +33,7 @@ body > .os-scrollbar {
.os-scrollbar-handle-interactive,
.os-scrollbar-track-interactive {
pointer-events: auto;
touch-action: none;
}
.os-scrollbar-unusable,
.os-scrollbar-unusable * {