@@ -1,12 +1,26 @@ -import { MutableRefObject, useEffect, useRef, useState } from 'react' +import { MutableRefObject } from 'react' export function useGenerallScroll(containerRef: MutableRefObject) { - function scrollToBottom() { + function scrollToBottomSmooth() { if (!containerRef.current) return - containerRef.current.style.transform = `translateY(0px)` - } + const el = containerRef.current + const start = el.scrollTop + const end = el.scrollHeight - el.clientHeight + const duration = 1800 + let startTime: number | null = null - return { - scrollToBottom, + function animateScroll(timestamp: number) { + if (!startTime) startTime = timestamp + const elapsed = timestamp - startTime + const progress = Math.min(elapsed / duration, 1) + el.scrollTop = start + (end - start) * progress + if (progress < 1) { + requestAnimationFrame(animateScroll) + } else { + el.scrollTop = end + } + } + requestAnimationFrame(animateScroll) } + return { scrollToBottomSmooth } } @@ -1,11 +1,12 @@ -import { AsyncQueue } from '#/shared/classes' +// import { AsyncQueue } from '#/shared/classes' import { + MouseEvent, MutableRefObject, + TouchEvent, useCallback, useEffect, useRef, useState, - TouchEvent, WheelEventHandler, } from 'react' @@ -21,9 +22,13 @@ export function useListenersWheel( const velocityRef = useRef(0) const animationFrameRef = useRef(null) - const asyncQueue = new AsyncQueue() - const [draggbleHeight, setDraggbleHeight] = useState(100) + const isDraggingRef = useRef(false) + const startDragYRef = useRef(0) + const startTranslateYRef = useRef(0) + + const targetTranslateYRef = useRef(0) + const animationFrameScrollRef = useRef(null) function defineDraggbleHeight() { const container = containerRef.current @@ -91,24 +96,45 @@ export function useListenersWheel( step() }, [applyTransform, clampTranslateY]) + function animateToTarget() { + if (!containerRef.current || !scrollRef.current) return + clampTranslateY() + const diff = targetTranslateYRef.current - translateYRef.current + if (Math.abs(diff) < 1) { + translateYRef.current = targetTranslateYRef.current + applyTransform() + animationFrameScrollRef.current = null + return + } + translateYRef.current += diff * 0.015 + clampTranslateY() + applyTransform() + animationFrameScrollRef.current = requestAnimationFrame(animateToTarget) + } + const onWheelCallback = useCallback( - (e) => { + async (e) => { if (preventScroll.current) return + const scrollSpeed = 0.5 + const deltaY = e.deltaY * scrollSpeed - for (const num of Array.from({ length: Math.abs(e.deltaY) }, (_, index) => - e.deltaY > 0 ? index + 1 : -(index + 1) - )) { - asyncQueue.push(async () => { - requestAnimationFrame(async () => { - translateYRef.current -= num - onScrolling && onScrolling(translateYRef.current) - clampTranslateY() - applyTransform() - defineDraggbleHeight() - preventScroll.current = false - }) - }) + translateYRef.current -= deltaY + clampTranslateY() + applyTransform() + + if (animationFrameScrollRef.current) { + cancelAnimationFrame(animationFrameScrollRef.current) + animationFrameScrollRef.current = null + } + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + animationFrameRef.current = null } + + defineDraggbleHeight() + targetTranslateYRef.current = translateYRef.current + + onScrolling && onScrolling(translateYRef.current) }, [clampTranslateY, applyTransform] ) @@ -161,6 +187,96 @@ export function useListenersWheel( // animateInertia() }, [animateInertia]) + const handleThumbnailClick = useCallback( + (e: MouseEvent) => { + if (isDraggingRef.current) return + + const scroll = scrollRef.current + const draggable = draggbleRef.current + const container = containerRef.current + + if (!scroll || !draggable || !container) return + + const thumbnailRect = e.currentTarget.getBoundingClientRect() + const clickPosition = e.clientY - thumbnailRect.top + const thumbHeight = draggable.clientHeight + + const newThumbPosition = Math.max( + 0, + Math.min(clickPosition - thumbHeight / 2, scroll.clientHeight - thumbHeight) + ) + + const proportion = scroll.clientHeight / container.clientHeight + translateYRef.current = (scroll.clientHeight - newThumbPosition - thumbHeight) / proportion + + clampTranslateY() + applyTransform() + + onScrolling && onScrolling(translateYRef.current) + }, + [clampTranslateY, applyTransform] + ) + + const handleThumbMouseDown = useCallback( + (e: MouseEvent) => { + e.preventDefault() + e.stopPropagation() + + isDraggingRef.current = true + startDragYRef.current = e.clientY + startTranslateYRef.current = translateYRef.current + + if (animationFrameScrollRef.current) { + cancelAnimationFrame(animationFrameScrollRef.current) + animationFrameScrollRef.current = null + } + + const handleMouseMove = (moveEvent: MouseEvent) => { + if (!isDraggingRef.current) return + + const scroll = scrollRef.current + const container = containerRef.current + if (!scroll || !container) return + + const deltaY = moveEvent.clientY - startDragYRef.current + const proportion = scroll.clientHeight / container.clientHeight + + translateYRef.current = startTranslateYRef.current - deltaY / proportion + clampTranslateY() + targetTranslateYRef.current = translateYRef.current + applyTransform() + + onScrolling && onScrolling(translateYRef.current) + } + + const handleMouseUp = () => { + isDraggingRef.current = false + document.removeEventListener('mousemove', handleMouseMove as any) + document.removeEventListener('mouseup', handleMouseUp) + } + + document.addEventListener('mousemove', handleMouseMove as any) + document.addEventListener('mouseup', handleMouseUp) + }, + [clampTranslateY, applyTransform] + ) + + function scrollToBottom() { + const container = containerRef.current + const scroll = scrollRef.current + if (!container || !scroll) return + + const maxScroll = container.clientHeight - scroll.clientHeight + targetTranslateYRef.current = -maxScroll + if (targetTranslateYRef.current > 0) targetTranslateYRef.current = 0 + clampTranslateY() + if (!animationFrameScrollRef.current) { + animationFrameScrollRef.current = requestAnimationFrame(animateToTarget) + } + + onScrolling && onScrolling(0) + } + return { onWheelCallback, preventScroll, @@ -169,5 +285,9 @@ export function useListenersWheel( handleTouchStart, handleTouchMove, handleTouchEnd, + handleThumbnailClick, + handleThumbMouseDown, + isDraggingRef, + scrollToBottom, } } @@ -1,50 +1,61 @@ -import React, { - Children, - MutableRefObject, - TouchEvent, - forwardRef, - useEffect, - useImperativeHandle, - useMemo, - useRef, -} from 'react' -import styles from './common-scrollbar.module.scss' -import { c } from '#/shared/lib/helpers' +import React, { forwardRef, useImperativeHandle, useMemo, useRef, useEffect } from 'react' + import { useHovered, useListenersThumbnail, useListenersWheel } from '../model' import { useGenerallScroll } from '../model/use-generall-scroll' +import styles from './common-scrollbar.module.scss' + +import { c } from '#/shared/lib/helpers' + interface CommonScrollbarProps extends React.HTMLAttributes { blockedScrollTop?: boolean - + onScrolling?: (y: number) => void } export interface CommonScrollbarRef extends HTMLDivElement { scrollToBottom: () => void - getContainer: () => HTMLDivElement | null } export const CommonScrollbar = forwardRef( ({ children, className, blockedScrollTop = false, onScrolling, ...props }, ref) => { const containerRef = useRef(null) - const localRef = useRef(null) - const { scrollToBottom } = useGenerallScroll(containerRef) - const { draggbleRef } = useListenersThumbnail() const { onMounseEnter, onMouseLeave, hovered } = useHovered() - const { onWheelCallback, draggbleHeight, handleTouchStart, handleTouchMove, translateYRef } = - useListenersWheel(containerRef, draggbleRef, localRef, onScrolling) - - const translate = useMemo(() => translateYRef.current, [translateYRef]) + const { + onWheelCallback, + draggbleHeight, + handleTouchStart, + handleTouchMove, + translateYRef, + handleThumbnailClick, + handleThumbMouseDown, + scrollToBottom, + } = useListenersWheel(containerRef, draggbleRef, localRef, onScrolling) useImperativeHandle(ref, () => ({ ...localRef.current!, scrollToBottom, })) + useEffect(() => { + const node = localRef.current + if (!node) return + + function wheelHandler(e: WheelEvent) { + e.preventDefault() + onWheelCallback(e as any) + } + node.addEventListener('wheel', wheelHandler, { passive: false }) + + return () => { + node.removeEventListener('wheel', wheelHandler) + } + }, [onWheelCallback]) + return (
-
+
= memo(({ model }) => { } return ( - <> +
{createPortal( = memo(({ model }) => { -
- - - - -
- + +
) })