mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-21 20:10:37 +03:00
add click scrolling implementation
This commit is contained in:
@@ -9,9 +9,18 @@ import {
|
|||||||
XY,
|
XY,
|
||||||
selfCancelTimeout,
|
selfCancelTimeout,
|
||||||
parent,
|
parent,
|
||||||
|
closest,
|
||||||
|
rAF,
|
||||||
|
cAF,
|
||||||
|
push,
|
||||||
|
noop,
|
||||||
} from 'support';
|
} from 'support';
|
||||||
import { getEnvironment } from 'environment';
|
import { getEnvironment } from 'environment';
|
||||||
import { classNamesScrollbarInteraction, classNamesScrollbarWheel } from 'classnames';
|
import {
|
||||||
|
classNameScrollbarHandle,
|
||||||
|
classNamesScrollbarInteraction,
|
||||||
|
classNamesScrollbarWheel,
|
||||||
|
} from 'classnames';
|
||||||
import type { ReadonlyOptions } from 'options';
|
import type { ReadonlyOptions } from 'options';
|
||||||
import type { StructureSetupState } from 'setups';
|
import type { StructureSetupState } from 'setups';
|
||||||
import type {
|
import type {
|
||||||
@@ -28,11 +37,31 @@ export type ScrollbarsSetupEvents = (
|
|||||||
isHorizontal?: boolean
|
isHorizontal?: boolean
|
||||||
) => () => void;
|
) => () => void;
|
||||||
|
|
||||||
const { round } = Math;
|
const { round, max, sign } = Math;
|
||||||
const getClientOffset = (event: PointerEvent): XY<number> => ({
|
const animationCurrentTime = () => performance.now();
|
||||||
x: event.clientX,
|
const animateNumber = (
|
||||||
y: event.clientY,
|
from: number,
|
||||||
});
|
to: number,
|
||||||
|
duration: number,
|
||||||
|
onFrame: (progress: number, completed: boolean) => any
|
||||||
|
) => {
|
||||||
|
let animationFrameId = 0;
|
||||||
|
const timeStart = animationCurrentTime();
|
||||||
|
const frame = () => {
|
||||||
|
const timeNow = animationCurrentTime();
|
||||||
|
const timeElapsed = timeNow - timeStart;
|
||||||
|
const stopAnimation = timeElapsed >= duration;
|
||||||
|
const percent = 1 - (max(0, timeStart + duration - timeNow) / duration || 0);
|
||||||
|
const progress = (to - from) * percent + from;
|
||||||
|
const animationCompleted = stopAnimation || percent === 1;
|
||||||
|
|
||||||
|
onFrame(progress, animationCompleted);
|
||||||
|
|
||||||
|
animationFrameId = animationCompleted ? 0 : rAF!(frame);
|
||||||
|
};
|
||||||
|
frame();
|
||||||
|
return () => cAF!(animationFrameId);
|
||||||
|
};
|
||||||
const getScale = (element: HTMLElement): XY<number> => {
|
const getScale = (element: HTMLElement): XY<number> => {
|
||||||
const { width, height } = getBoundingClientRect(element);
|
const { width, height } = getBoundingClientRect(element);
|
||||||
const { w, h } = offsetSize(element);
|
const { w, h } = offsetSize(element);
|
||||||
@@ -44,7 +73,7 @@ const getScale = (element: HTMLElement): XY<number> => {
|
|||||||
const continuePointerDown = (
|
const continuePointerDown = (
|
||||||
event: PointerEvent,
|
event: PointerEvent,
|
||||||
options: ReadonlyOptions,
|
options: ReadonlyOptions,
|
||||||
scrollType: 'dragScroll' | 'clickScroll'
|
isDragScroll: boolean
|
||||||
) => {
|
) => {
|
||||||
const scrollbarOptions = options.scrollbars;
|
const scrollbarOptions = options.scrollbars;
|
||||||
const { button, isPrimary, pointerType } = event;
|
const { button, isPrimary, pointerType } = event;
|
||||||
@@ -52,7 +81,7 @@ const continuePointerDown = (
|
|||||||
return (
|
return (
|
||||||
button === 0 &&
|
button === 0 &&
|
||||||
isPrimary &&
|
isPrimary &&
|
||||||
scrollbarOptions[scrollType] &&
|
scrollbarOptions[isDragScroll ? 'dragScroll' : 'clickScroll'] &&
|
||||||
(pointers || []).includes(pointerType)
|
(pointers || []).includes(pointerType)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -63,7 +92,7 @@ const createRootClickStopPropagationEvents = (scrollbar: HTMLElement, documentEl
|
|||||||
on.bind(0, documentElm, 'click', stopPropagation, { _once: true, _capture: true }),
|
on.bind(0, documentElm, 'click', stopPropagation, { _once: true, _capture: true }),
|
||||||
{ _capture: true }
|
{ _capture: true }
|
||||||
);
|
);
|
||||||
const createDragScrollingEvents = (
|
const createInteractiveScrollEvents = (
|
||||||
options: ReadonlyOptions,
|
options: ReadonlyOptions,
|
||||||
doc: Document,
|
doc: Document,
|
||||||
scrollbarStructure: ScrollbarStructure,
|
scrollbarStructure: ScrollbarStructure,
|
||||||
@@ -73,51 +102,107 @@ const createDragScrollingEvents = (
|
|||||||
) => {
|
) => {
|
||||||
const { _rtlScrollBehavior } = getEnvironment();
|
const { _rtlScrollBehavior } = getEnvironment();
|
||||||
const { _handle, _track, _scrollbar } = scrollbarStructure;
|
const { _handle, _track, _scrollbar } = scrollbarStructure;
|
||||||
const scrollOffsetKey = `scroll${isHorizontal ? 'Left' : 'Top'}`;
|
const scrollLeftTopKey = `scroll${isHorizontal ? 'Left' : 'Top'}`;
|
||||||
const xyKey = `${isHorizontal ? 'x' : 'y'}`;
|
const widthHeightKey = isHorizontal ? 'width' : 'height';
|
||||||
const whKey = `${isHorizontal ? 'w' : 'h'}`;
|
const whKey = isHorizontal ? 'w' : 'h';
|
||||||
const createOnPointerMoveHandler =
|
const xyKey = isHorizontal ? 'x' : 'y';
|
||||||
(mouseDownScroll: number, mouseDownClientOffset: number, mouseDownInvertedScale: number) =>
|
const getHandleOffset = (handleRect: DOMRect, trackRect: DOMRect) =>
|
||||||
(event: PointerEvent) => {
|
handleRect[xyKey] - trackRect[xyKey];
|
||||||
|
const createRelativeHandleMove =
|
||||||
|
(mouseDownScroll: number, invertedScale: number) => (deltaMovement: number) => {
|
||||||
const { _overflowAmount } = structureSetupState();
|
const { _overflowAmount } = structureSetupState();
|
||||||
const movement =
|
|
||||||
(getClientOffset(event)[xyKey] - mouseDownClientOffset) * mouseDownInvertedScale;
|
|
||||||
const handleTrackDiff = offsetSize(_track)[whKey] - offsetSize(_handle)[whKey];
|
const handleTrackDiff = offsetSize(_track)[whKey] - offsetSize(_handle)[whKey];
|
||||||
const scrollDeltaPercent = movement / handleTrackDiff;
|
const scrollDeltaPercent = (invertedScale * deltaMovement) / handleTrackDiff;
|
||||||
const scrollDelta = scrollDeltaPercent * _overflowAmount[xyKey];
|
const scrollDelta = scrollDeltaPercent * _overflowAmount[xyKey];
|
||||||
const isRTL = directionIsRTL(_scrollbar);
|
const isRTL = directionIsRTL(_scrollbar);
|
||||||
const negateMultiplactor =
|
const negateMultiplactor =
|
||||||
isRTL && isHorizontal ? (_rtlScrollBehavior.n || _rtlScrollBehavior.i ? 1 : -1) : 1;
|
isRTL && isHorizontal ? (_rtlScrollBehavior.n || _rtlScrollBehavior.i ? 1 : -1) : 1;
|
||||||
|
|
||||||
scrollOffsetElement[scrollOffsetKey] = mouseDownScroll + scrollDelta * negateMultiplactor;
|
scrollOffsetElement[scrollLeftTopKey] = mouseDownScroll + scrollDelta * negateMultiplactor;
|
||||||
};
|
};
|
||||||
|
|
||||||
return on(_handle, 'pointerdown', (pointerDownEvent: PointerEvent) => {
|
return on(_track, 'pointerdown', (pointerDownEvent: PointerEvent) => {
|
||||||
if (continuePointerDown(pointerDownEvent, options, 'dragScroll')) {
|
const isDragScroll =
|
||||||
const offSelectStart = on(doc, 'selectstart', (event: Event) => preventDefault(event), {
|
closest(pointerDownEvent.target as Node, `.${classNameScrollbarHandle}`) === _handle;
|
||||||
_passive: false,
|
|
||||||
});
|
if (continuePointerDown(pointerDownEvent, options, isDragScroll)) {
|
||||||
const offPointerMove = on(
|
const instantClickScroll = !isDragScroll && pointerDownEvent.shiftKey;
|
||||||
_handle,
|
const moveHandleRelative = createRelativeHandleMove(
|
||||||
'pointermove',
|
scrollOffsetElement[scrollLeftTopKey] || 0,
|
||||||
createOnPointerMoveHandler(
|
1 / getScale(scrollOffsetElement)[xyKey]
|
||||||
scrollOffsetElement[scrollOffsetKey] || 0,
|
|
||||||
getClientOffset(pointerDownEvent)[xyKey],
|
|
||||||
1 / getScale(scrollOffsetElement)[xyKey]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
const pointerDownOffset = pointerDownEvent[xyKey];
|
||||||
|
const handleRect = getBoundingClientRect(_handle);
|
||||||
|
const trackRect = getBoundingClientRect(_track);
|
||||||
|
const handleLength = handleRect[widthHeightKey];
|
||||||
|
const handleCenter = getHandleOffset(handleRect, trackRect) + handleLength / 2;
|
||||||
|
const relativeTrackPointerOffset = pointerDownOffset - trackRect[xyKey];
|
||||||
|
const startOffset = isDragScroll ? 0 : relativeTrackPointerOffset - handleCenter;
|
||||||
|
|
||||||
|
const offFns = [
|
||||||
|
on(doc, 'selectstart', (event: Event) => preventDefault(event), {
|
||||||
|
_passive: false,
|
||||||
|
}),
|
||||||
|
on(_track, 'pointermove', (pointerMoveEvent: PointerEvent) => {
|
||||||
|
const relativeMovement = pointerMoveEvent[xyKey] - pointerDownOffset;
|
||||||
|
|
||||||
|
if (isDragScroll || instantClickScroll) {
|
||||||
|
moveHandleRelative(startOffset + relativeMovement);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (instantClickScroll) {
|
||||||
|
moveHandleRelative(startOffset);
|
||||||
|
} else if (!isDragScroll) {
|
||||||
|
// click scroll animation
|
||||||
|
let iteration = 0;
|
||||||
|
let clear = noop;
|
||||||
|
const animateClickScroll = (clickScrollProgress: number) => {
|
||||||
|
clear = animateNumber(
|
||||||
|
clickScrollProgress,
|
||||||
|
clickScrollProgress + handleLength * sign(startOffset),
|
||||||
|
133,
|
||||||
|
(animationProgress, animationCompleted) => {
|
||||||
|
moveHandleRelative(animationProgress);
|
||||||
|
const handleStartBound = getHandleOffset(getBoundingClientRect(_handle), trackRect);
|
||||||
|
const handleEndBound = handleStartBound + handleLength;
|
||||||
|
const mouseBetweenHandleBounds =
|
||||||
|
relativeTrackPointerOffset >= handleStartBound &&
|
||||||
|
relativeTrackPointerOffset <= handleEndBound;
|
||||||
|
|
||||||
|
if (animationCompleted && !mouseBetweenHandleBounds) {
|
||||||
|
if (iteration) {
|
||||||
|
animateClickScroll(animationProgress);
|
||||||
|
} else {
|
||||||
|
const firstIterationPauseTimeout = setTimeout(() => {
|
||||||
|
animateClickScroll(animationProgress);
|
||||||
|
}, 222);
|
||||||
|
clear = () => {
|
||||||
|
clearTimeout(firstIterationPauseTimeout);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
iteration++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
animateClickScroll(0);
|
||||||
|
|
||||||
|
push(offFns, () => clear());
|
||||||
|
}
|
||||||
|
|
||||||
on(
|
on(
|
||||||
_handle,
|
_track,
|
||||||
'pointerup',
|
'pointerup',
|
||||||
(pointerUpEvent: PointerEvent) => {
|
(pointerUpEvent: PointerEvent) => {
|
||||||
offSelectStart();
|
runEachAndClear(offFns);
|
||||||
offPointerMove();
|
_track.releasePointerCapture(pointerUpEvent.pointerId);
|
||||||
_handle.releasePointerCapture(pointerUpEvent.pointerId);
|
|
||||||
},
|
},
|
||||||
{ _once: true }
|
{ _once: true }
|
||||||
);
|
);
|
||||||
_handle.setPointerCapture(pointerDownEvent.pointerId);
|
_track.setPointerCapture(pointerDownEvent.pointerId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -174,7 +259,7 @@ export const createScrollbarsSetupEvents =
|
|||||||
{ _passive: false, _capture: true }
|
{ _passive: false, _capture: true }
|
||||||
),
|
),
|
||||||
createRootClickStopPropagationEvents(_scrollbar, documentElm),
|
createRootClickStopPropagationEvents(_scrollbar, documentElm),
|
||||||
createDragScrollingEvents(
|
createInteractiveScrollEvents(
|
||||||
options,
|
options,
|
||||||
documentElm,
|
documentElm,
|
||||||
scrollbarStructure,
|
scrollbarStructure,
|
||||||
|
|||||||
@@ -66,11 +66,13 @@ body > .os-scrollbar {
|
|||||||
.os-scrollbar-unusable .os-scrollbar-handle {
|
.os-scrollbar-unusable .os-scrollbar-handle {
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
}
|
}
|
||||||
.os-scrollbar.os-scrollbar-horizontal.os-scrollbar-cornerless {
|
.os-scrollbar.os-scrollbar-horizontal.os-scrollbar-cornerless,
|
||||||
|
.os-scrollbar.os-scrollbar-horizontal.os-scrollbar-cornerless.os-scrollbar-rtl {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
.os-scrollbar.os-scrollbar-vertical.os-scrollbar-cornerless {
|
.os-scrollbar.os-scrollbar-vertical.os-scrollbar-cornerless,
|
||||||
|
.os-scrollbar.os-scrollbar-vertical.os-scrollbar-cornerless.os-scrollbar-rtl {
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -43,6 +43,11 @@ window.OverlayScrollbars = OverlayScrollbars;
|
|||||||
OverlayScrollbars.env().setDefaultInitialization({
|
OverlayScrollbars.env().setDefaultInitialization({
|
||||||
cancel: { nativeScrollbarsOverlaid: false },
|
cancel: { nativeScrollbarsOverlaid: false },
|
||||||
});
|
});
|
||||||
|
OverlayScrollbars.env().setDefaultOptions({
|
||||||
|
scrollbars: {
|
||||||
|
clickScroll: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
interface Metrics {
|
interface Metrics {
|
||||||
offset: {
|
offset: {
|
||||||
|
|||||||
Reference in New Issue
Block a user