diff --git a/src/shared/ui-kit/box/Virtual/hooks/useVirtualList/useVirtualList.ts b/src/shared/ui-kit/box/Virtual/hooks/useVirtualList/useVirtualList.ts index 735382e5..f02f5359 100644 --- a/src/shared/ui-kit/box/Virtual/hooks/useVirtualList/useVirtualList.ts +++ b/src/shared/ui-kit/box/Virtual/hooks/useVirtualList/useVirtualList.ts @@ -28,6 +28,7 @@ export const useVirtualList = function (props: UseVirtualListProps) { ); const setIndex = useCallback((index: number) => { + console.log('set index', index); currentIndex.current = index; setVirtualList( getVirtualList({ index, list, showAmount }), diff --git a/src/shared/ui-kit/box/Virtual/ui/Virtual.module.scss b/src/shared/ui-kit/box/Virtual/ui/Virtual.module.scss index 05f53b49..71ba8a7e 100644 --- a/src/shared/ui-kit/box/Virtual/ui/Virtual.module.scss +++ b/src/shared/ui-kit/box/Virtual/ui/Virtual.module.scss @@ -40,6 +40,19 @@ } } + .loader { + visibility : hidden; + opacity : 0; + height : 0; + + &.loading { + opacity : 1; + visibility : visible; + height : fit-content; + width : fit-content; + } + } + &.top { .scrollContainer, .scrollBar { diff --git a/src/shared/ui-kit/box/Virtual/ui/Virtual.tsx b/src/shared/ui-kit/box/Virtual/ui/Virtual.tsx index f70f12c9..dac38917 100644 --- a/src/shared/ui-kit/box/Virtual/ui/Virtual.tsx +++ b/src/shared/ui-kit/box/Virtual/ui/Virtual.tsx @@ -6,7 +6,7 @@ import { memo, ReactNode, useCallback, useEffect, - useLayoutEffect, + useLayoutEffect, useMemo, useRef, useState, } from 'react'; import classNames from 'classnames'; @@ -89,6 +89,13 @@ export const Virtual: FC = memo(function Virtual (props) { ...other } = props; + /******************************************************************* + * Variables + ******************************************************************/ + + const toggleDistance = useMemo(() => Math.ceil(showAmount / 4), [ showAmount ]); + + /******************************************************************* * Items ******************************************************************/ @@ -101,15 +108,16 @@ export const Virtual: FC = memo(function Virtual (props) { /******************************************************************* - * Scroll + * Wheel scroll, scroll animation, scrollTo ******************************************************************/ const containerRef = useRef(null); const targetScrollPosition = useRef(0); const startAnimationTime = useRef(0); const startAnimationPosition = useRef(0); + const requestAnimation = useRef(0); - const scrollAnimation = useCallback((timestamp: number) => { + const scrollAnimation = useCallback((ref: HTMLDivElement, timestamp: number) => { if (startAnimationTime.current === 0) { startAnimationTime.current = timestamp; } @@ -122,10 +130,25 @@ export const Virtual: FC = memo(function Virtual (props) { startAnimationTime : startAnimationTime.current, }); - containerRef.current.scrollTop = scrollPosition; + ref.scrollTop = scrollPosition; if (scrollPosition !== targetScrollPosition.current) { - requestAnimationFrame(scrollAnimation); + requestAnimation.current = requestAnimationFrame((t) => scrollAnimation(ref, t)); + } + }, [ animationMs ]); + + const scrollTo = useCallback((target: number, smooth: boolean) => { + const ref = containerRef.current; + if (ref) { + if (smooth) { + targetScrollPosition.current = target; + startAnimationPosition.current = ref.scrollTop; + startAnimationTime.current = 0; + requestAnimation.current = requestAnimationFrame((t) => scrollAnimation(ref, t)); + } else { + cancelAnimationFrame(requestAnimation.current); + ref.scrollTop = target; + } } }, []); @@ -134,7 +157,7 @@ export const Virtual: FC = memo(function Virtual (props) { if (ref) { const onWheelHandler = function (event: WheelEvent) { - const { scrollTop, scrollHeight, offsetHeight } = ref; + const { scrollHeight, offsetHeight } = ref; const targetPosition = getScrollTargetScrollPosition({ scrollDistance, @@ -146,10 +169,7 @@ export const Virtual: FC = memo(function Virtual (props) { }); if (targetPosition !== null) { - targetScrollPosition.current = targetPosition; - startAnimationPosition.current = scrollTop; - startAnimationTime.current = 0; - requestAnimationFrame(scrollAnimation); + scrollTo(targetPosition, true); } }; @@ -169,6 +189,52 @@ export const Virtual: FC = memo(function Virtual (props) { const previousScrollAction = useRef(VirtualAction.AUTOSCROLL_NEXT); const disableScrollHandler = useRef(false); + const saveCurrentFirstElement = useCallback((content: HTMLDivElement) => { + const firstElement = content.firstElementChild as HTMLElement; + + if (firstElement) { + previousFirstElementPosition.current = firstElement.offsetTop; + previousFirstElement.current = firstElement; + disableScrollHandler.current = true; + } + }, []); + + const saveCurrentLastElement = useCallback((content: HTMLDivElement) => { + const lastElement = content.lastElementChild as HTMLElement; + + if (lastElement) { + previousLastElementPosition.current = lastElement.offsetTop; + previousLastElement.current = lastElement; + disableScrollHandler.current = true; + } + }, []); + + const toggleNext = useCallback((content: HTMLDivElement) => { + previousScrollAction.current = VirtualAction.TOGGLE_NEXT; + previousContentHeight.current = content.scrollHeight; + + if (isTop(type)) { + saveCurrentFirstElement(content); + setIndex(Math.max(0, currentIndex.current - toggleDistance)); + } else { + saveCurrentLastElement(content); + setIndex(Math.min(Math.max(0, list.length - showAmount), currentIndex.current + toggleDistance)); + } + }, [ type, setIndex, toggleDistance, saveCurrentFirstElement, saveCurrentLastElement ]); + + const togglePrevious = useCallback((content: HTMLDivElement) => { + previousScrollAction.current = VirtualAction.TOGGLE_PREVIOUS; + previousContentHeight.current = content.scrollHeight; + + if (isTop(type)) { + saveCurrentLastElement(content); + setIndex(Math.min(Math.max(0, list.length - showAmount), currentIndex.current + toggleDistance)); + } else { + saveCurrentFirstElement(content); + setIndex(Math.max(0, currentIndex.current - toggleDistance)); + } + }, [ type, setIndex, toggleDistance, saveCurrentFirstElement, saveCurrentLastElement ]); + useLayoutEffect(() => { const ref = containerRef.current; const content = contentRef.current; @@ -186,46 +252,38 @@ export const Virtual: FC = memo(function Virtual (props) { previousScrollTop: previousScrollTop.current, distanceToTrigger, type, - }) && previousScrollAction.current === VirtualAction.NONE) { - if (isTop(type)) { - if (currentIndex.current !== 0) { - previousScrollAction.current = VirtualAction.TOGGLE_NEXT; - previousContentHeight.current = content.scrollHeight; - const firstElement = content.firstElementChild as HTMLElement; - previousFirstElementPosition.current = firstElement.offsetTop; - previousFirstElement.current = firstElement; - disableScrollHandler.current = true; - setIndex(Math.max(0, currentIndex.current - 10)); - } else if (uploadNext && hasMoreNext) { - if (!loadingNext) { - previousContentHeight.current = content.scrollHeight; - previousScrollAction.current = VirtualAction.TOGGLE_NEXT; - uploadNext(); + })) { + switch (previousScrollAction.current) { + case VirtualAction.AUTOSCROLL_NEXT: + case VirtualAction.TOGGLE_NEXT: + break; + default: + let isNotStartList: boolean; + + if (isTop(type)) { + isNotStartList = currentIndex.current !== 0; + } else { + isNotStartList = currentIndex.current < list.length - showAmount; } - } else { - previousContentHeight.current = content.scrollHeight; - previousScrollAction.current = VirtualAction.AUTOSCROLL_NEXT; - } - } else { - if (currentIndex.current < list.length - showAmount) { - previousScrollAction.current = VirtualAction.TOGGLE_NEXT; - previousContentHeight.current = content.scrollHeight; - const lastElement = content.lastElementChild as HTMLElement; - previousLastElementPosition.current = lastElement.offsetTop; - previousLastElement.current = lastElement; - disableScrollHandler.current = true; - setIndex(Math.min(Math.max(0, list.length - showAmount), currentIndex.current + 10)); - } else if (uploadNext && hasMoreNext) { - if (!loadingNext) { + + if (isNotStartList) { + toggleNext(content); + break; + } + + const needUploadNext = hasMoreNext && uploadNext; + if (needUploadNext) { previousContentHeight.current = content.scrollHeight; previousScrollAction.current = VirtualAction.TOGGLE_NEXT; - uploadNext(); + + if (!loadingNext) { + uploadNext(); + } + break; } - } else { - console.log('SET AUTOSCROLL NEXT', content); - previousContentHeight.current = content.scrollHeight; - previousScrollAction.current = VirtualAction.AUTOSCROLL_NEXT; - } + + previousScrollAction.current = VirtualAction.AUTOSCROLL_NEXT; + break; } } else if (isPreviousScrollPosition({ scrollTop, @@ -234,24 +292,40 @@ export const Virtual: FC = memo(function Virtual (props) { previousScrollTop: previousScrollTop.current, distanceToTrigger, type, - }) && previousScrollAction.current !== VirtualAction.TOGGLE_PREVIOUS) { - previousScrollAction.current = VirtualAction.TOGGLE_PREVIOUS; - previousContentHeight.current = content.scrollHeight; - if (isTop(type)) { - const lastElement = content.lastElementChild as HTMLElement; - previousLastElementPosition.current = lastElement.offsetTop; - previousLastElement.current = lastElement; - disableScrollHandler.current = true; - setIndex(Math.min(Math.max(0, list.length - showAmount), currentIndex.current + 10)); - } else { - const firstElement = content.firstElementChild as HTMLElement; - previousFirstElementPosition.current = firstElement.offsetTop; - previousFirstElement.current = firstElement; - disableScrollHandler.current = true; - setIndex(Math.max(0, currentIndex.current - 10)); + })) { + switch (previousScrollAction.current) { + case VirtualAction.AUTOSCROLL_PREVIOUS: + case VirtualAction.TOGGLE_PREVIOUS: + break; + default: + let isNotEndList: boolean; + + if (isTop(type)) { + isNotEndList = currentIndex.current < list.length - showAmount; + } else { + isNotEndList = currentIndex.current !== 0; + } + + if (isNotEndList) { + togglePrevious(content); + break; + } + + const needUploadPrevious = hasMorePrevious && uploadPrevious; + if (needUploadPrevious) { + previousContentHeight.current = content.scrollHeight; + previousScrollAction.current = VirtualAction.TOGGLE_PREVIOUS; + + if (!loadingPrevious) { + uploadPrevious(); + } + break; + } + + previousScrollAction.current = VirtualAction.AUTOSCROLL_PREVIOUS; + break; } } else if (previousScrollTop.current !== scrollTop) { - console.log('SET AUTOSCROLL NONE'); previousScrollAction.current = VirtualAction.NONE; } @@ -279,6 +353,56 @@ export const Virtual: FC = memo(function Virtual (props) { const previousLastElementPosition = useRef(0); const previousContentHeight = useRef(null); + const applyOffsetToCurrentScrollPosition = useCallback((ref: HTMLDivElement, offset: number) => { + ref.scrollTop = previousScrollTop.current + offset; + targetScrollPosition.current = targetScrollPosition.current + offset; + startAnimationPosition.current = startAnimationPosition.current + offset; + }, []); + + const scrollByFirstElement = useCallback((content: HTMLDivElement, ref: HTMLDivElement) => { + const firstElement = content.firstElementChild as HTMLElement; + const previousElement = previousFirstElement.current; + + if (firstElement !== previousElement && previousElement !== null) { + const currentPosition = previousElement?.offsetTop; + const positionDelta = currentPosition - previousFirstElementPosition.current; + applyOffsetToCurrentScrollPosition(ref, positionDelta); + } + }, []); + + const scrollByLastElement = useCallback((content: HTMLDivElement, ref: HTMLDivElement) => { + const lastElement = content.lastElementChild as HTMLElement; + const previousElement = previousLastElement.current; + + if (lastElement !== previousElement && previousElement !== null) { + const currentPosition = previousElement?.offsetTop; + const positionDelta = currentPosition - previousLastElementPosition.current; + applyOffsetToCurrentScrollPosition(ref, positionDelta); + } + }, []); + + const updatePreviousFirstElement = useCallback(() => { + const content = contentRef.current; + if (content) { + const firstElement = content.firstElementChild as HTMLElement; + if (firstElement) { + previousFirstElement.current = firstElement; + previousFirstElementPosition.current = firstElement.offsetTop; + } + } + }, []); + + const updatePreviousLastElement = useCallback(() => { + const content = contentRef.current; + if (content) { + const lastElement = content.lastElementChild as HTMLElement; + if (lastElement) { + previousLastElement.current = lastElement; + previousLastElementPosition.current = lastElement.offsetTop; + } + } + }, []); + useEffect(() => { const ref = containerRef.current; const content = contentRef.current; @@ -287,97 +411,57 @@ export const Virtual: FC = memo(function Virtual (props) { switch (previousScrollAction.current) { case VirtualAction.TOGGLE_NEXT: if (isTop(type)) { - const firstElement = content.firstElementChild as HTMLElement; - const previousElement = previousFirstElement.current; - - if (firstElement !== previousElement) { - const currentPosition = previousElement.offsetTop; - const positionDelta = currentPosition - previousFirstElementPosition.current; - ref.scrollTop = previousScrollTop.current + positionDelta; - targetScrollPosition.current = targetScrollPosition.current + positionDelta; - startAnimationPosition.current = startAnimationPosition.current + positionDelta; - } + scrollByFirstElement(content, ref); } else { - const lastElement = content.lastElementChild as HTMLElement; - const previousElement = previousLastElement.current; - - if (lastElement !== previousElement) { - const currentPosition = previousElement.offsetTop; - const positionDelta = currentPosition - previousLastElementPosition.current; - ref.scrollTop = previousScrollTop.current + positionDelta; - targetScrollPosition.current = targetScrollPosition.current + positionDelta; - startAnimationPosition.current = startAnimationPosition.current + positionDelta; - } + scrollByLastElement(content, ref); } disableScrollHandler.current = false; break; case VirtualAction.TOGGLE_PREVIOUS: if (isTop(type)) { - const lastElement = content.lastElementChild as HTMLElement; - const previousElement = previousLastElement.current; - - if (lastElement !== previousElement && previousElement !== null) { - const currentPosition = previousElement.offsetTop; - const positionDelta = currentPosition - previousLastElementPosition.current; - ref.scrollTop = previousScrollTop.current + positionDelta; - targetScrollPosition.current = targetScrollPosition.current + positionDelta; - startAnimationPosition.current = startAnimationPosition.current + positionDelta; - } + scrollByLastElement(content, ref); } else { - const firstElement = content.firstElementChild as HTMLElement; - const previousElement = previousFirstElement.current; - - if (firstElement !== previousElement && previousElement !== null) { - const currentPosition = previousElement.offsetTop; - const positionDelta = currentPosition - previousFirstElementPosition.current; - ref.scrollTop = previousScrollTop.current + positionDelta; - targetScrollPosition.current = targetScrollPosition.current + positionDelta; - startAnimationPosition.current = startAnimationPosition.current + positionDelta; - } + scrollByFirstElement(content, ref); } disableScrollHandler.current = false; break; case VirtualAction.AUTOSCROLL_NEXT: - console.log('AUTOSCROLL NEXT'); if (isTop(type)) { - const firstElement = content.firstElementChild as HTMLElement; - const previousElement = previousFirstElement.current; - - if (firstElement !== previousElement && previousElement !== null) { - const currentPosition = previousElement.offsetTop; - const positionDelta = currentPosition - previousFirstElementPosition.current; - ref.scrollTop = previousScrollTop.current + positionDelta; - } + scrollByFirstElement(content, ref); + setTimeout(() => { + scrollTo(0, smoothAutoscroll); + disableScrollHandler.current = false; + }); } else { - const lastElement = content.lastElementChild as HTMLElement; - const previousElement = previousLastElement.current; - - console.log('Last element', lastElement); - console.log('PreviousElement', previousElement); - - if (lastElement !== previousElement && previousElement !== null) { - const currentPosition = previousElement.offsetTop; - const positionDelta = currentPosition - previousLastElementPosition.current; - ref.scrollTop = previousScrollTop.current + positionDelta; - console.log('scroll top ->', ref.scrollTop); - setTimeout(() => { - console.log('scroll'); - targetScrollPosition.current = 0; - startAnimationPosition.current = ref.scrollTop; - startAnimationTime.current = 0; - requestAnimationFrame(scrollAnimation); - }); - } + scrollByLastElement(content, ref); + setTimeout(() => { + scrollTo(0, smoothAutoscroll); + disableScrollHandler.current = false; + }); } - disableScrollHandler.current = false; break; case VirtualAction.AUTOSCROLL_PREVIOUS: + if (isTop(type)) { + scrollByLastElement(content, ref); + setTimeout(() => { + scrollTo(ref.scrollHeight - ref.offsetHeight, smoothAutoscroll); + disableScrollHandler.current = false; + }); + } else { + setTimeout(() => { + scrollTo(-(ref.scrollHeight - ref.offsetHeight), smoothAutoscroll); + disableScrollHandler.current = false; + }); + } break; default: disableScrollHandler.current = false; break; } + + updatePreviousFirstElement(); + updatePreviousLastElement(); } }, [ virtualList ]); @@ -387,13 +471,13 @@ export const Virtual: FC = memo(function Virtual (props) { ******************************************************************/ const scrollBarRef = useRef(null); - const scrollMarketRef = useRef(null); + const scrollMarkerRef = useRef(null); const [ scrollable, setScrollable ] = useState(false); useLayoutEffect(() => { const ref = containerRef.current; const bar = scrollBarRef.current; - const marker = scrollMarketRef.current; + const marker = scrollMarkerRef.current; if (ref && bar && marker) { if (ref.scrollHeight === ref.offsetHeight && scrollable === true) { @@ -411,8 +495,6 @@ export const Virtual: FC = memo(function Virtual (props) { const markerOffset = (barHeight - markerHeight) / 100 * percentOfScroll; marker.style.transform = `translateY(${ markerOffset }px)`; - // get position of marker - // set position to marker }; ref.addEventListener('scroll', onScrollHandler); @@ -422,6 +504,18 @@ export const Virtual: FC = memo(function Virtual (props) { } }, []); + useLayoutEffect(() => { + const ref = containerRef.current; + + if (ref) { + if (ref.scrollHeight === ref.offsetHeight && scrollable === true) { + setScrollable(false); + } else if (ref.scrollHeight !== ref.offsetHeight && scrollable === false) { + setScrollable(true); + } + } + }, [ list ]); + /******************************************************************* * On main list change @@ -435,12 +529,10 @@ export const Virtual: FC = memo(function Virtual (props) { const content = contentRef.current; if (content) { - const listLengthChanged = previousListLength.current !== list.length; - - console.log('UPDATOR:', previousScrollAction.current, list); + const lengthDelta: number = list.length - previousListLength.current; + const justRefresh = lengthDelta <= 0; - if (!listLengthChanged) { - // Just refresh + if (justRefresh) { setIndex(currentIndex.current); } @@ -450,36 +542,50 @@ export const Virtual: FC = memo(function Virtual (props) { switch (previousScrollAction.current) { case VirtualAction.AUTOSCROLL_NEXT: if (isTop(type)) { - console.log('firs titem', firstItem); + disableScrollHandler.current = true; + setIndex(0); + } else { + disableScrollHandler.current = true; + setIndex(Math.max(0, list.length - showAmount)); + } + break; + case VirtualAction.AUTOSCROLL_PREVIOUS: + break; + case VirtualAction.TOGGLE_PREVIOUS: + if (isTop(type)) { + if (lastItem !== previousLastListItem.current) { + disableScrollHandler.current = true; + setIndex(Math.max(list.length - showAmount, currentIndex.current + toggleDistance)); + } + } else { if (firstItem !== previousFirstListItem.current) { - const firstElement = content.firstElementChild as HTMLElement; - previousFirstElementPosition.current = firstElement.offsetTop; - previousFirstElement.current = firstElement; - disableScrollHandler.current = true; - setIndex(0); + disableScrollHandler.current = true; + setIndex(Math.min(Math.max(0, list.length - showAmount), currentIndex.current + lengthDelta - toggleDistance)); + } + } + break; + case VirtualAction.TOGGLE_NEXT: + if (isTop(type)) { + if (firstItem !== previousFirstListItem.current) { + disableScrollHandler.current = true; + setIndex(Math.max(list.length - showAmount, currentIndex.current + toggleDistance)); } } else { - console.log('last item', lastItem); if (lastItem !== previousLastListItem.current) { - const lastElement = content.lastElementChild as HTMLElement; - previousLastElementPosition.current = lastElement.offsetTop; - previousLastElement.current = lastElement; - disableScrollHandler.current = true; - setIndex(Math.max(0, list.length - showAmount)); + disableScrollHandler.current = true; + setIndex(Math.min(Math.max(0, list.length - showAmount), currentIndex.current + lengthDelta - toggleDistance)); } } break; - case VirtualAction.AUTOSCROLL_PREVIOUS: - break; default: break; } + previousListLength.current = list.length; previousLastListItem.current = lastItem; previousFirstListItem.current = firstItem; } - }, [ list ]); - + }, [ list, toggleDistance ]); return (
= memo(function Virtual (props) { className={ css.scrollContainer } ref={ containerRef } > + { !hasMoreNext ? noMoreNextElement : null }
{ virtualList.map(render) }
+ { !hasMorePrevious ? noMorePreviousElement : null }
-
+
); diff --git a/src/widgets/message/PrivateMessagesInfinityVirtualContainer/ui/PrivateMessagesInfinityVirtualContainer.tsx b/src/widgets/message/PrivateMessagesInfinityVirtualContainer/ui/PrivateMessagesInfinityVirtualContainer.tsx index 753bcbd8..741e1871 100644 --- a/src/widgets/message/PrivateMessagesInfinityVirtualContainer/ui/PrivateMessagesInfinityVirtualContainer.tsx +++ b/src/widgets/message/PrivateMessagesInfinityVirtualContainer/ui/PrivateMessagesInfinityVirtualContainer.tsx @@ -27,6 +27,7 @@ import { VirtualType, } from '@/shared/ui-kit/box/Virtual/types/types.ts'; import { Virtual } from '@/shared/ui-kit/box/Virtual/ui/Virtual.tsx'; +import { Loader } from '@/shared/ui-kit/loaders/Loader/ui/Loader.tsx'; export type PrivateMessagesInfinityVirtualContainerProps = @@ -47,7 +48,7 @@ export const PrivateMessagesInfinityVirtualContainer: FC } + loaderPreviousElement={ } + loadingNext={ false } loadingPrevious={ messagesPending[dialogueId] } + noMoreNextElement="[Пользователь] набирает сообщение..." + noMorePreviousElement="Сообщений больше нет :(" render={ render } showAmount={ 40 } smoothAutoscroll