1
This commit is contained in:
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import * as React from 'react';
|
||||
import type { RenderFunc, SharedConfig } from '../interface';
|
||||
export default function useChildren<T>(list: T[], startIndex: number, endIndex: number, scrollWidth: number, offsetX: number, setNodeRef: (item: T, element: HTMLElement) => void, renderFunc: RenderFunc<T>, { getKey }: SharedConfig<T>): React.JSX.Element[];
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import { Item } from "../Item";
|
||||
export default function useChildren(list, startIndex, endIndex, scrollWidth, offsetX, setNodeRef, renderFunc, {
|
||||
getKey
|
||||
}) {
|
||||
return list.slice(startIndex, endIndex + 1).map((item, index) => {
|
||||
const eleIndex = startIndex + index;
|
||||
const node = renderFunc(item, eleIndex, {
|
||||
style: {
|
||||
width: scrollWidth
|
||||
},
|
||||
offsetX
|
||||
});
|
||||
const key = getKey(item);
|
||||
return /*#__PURE__*/React.createElement(Item, {
|
||||
key: key,
|
||||
setRef: ele => setNodeRef(item, ele)
|
||||
}, node);
|
||||
});
|
||||
}
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import type { GetKey } from '../interface';
|
||||
export default function useDiffItem<T>(data: T[], getKey: GetKey<T>, onDiff?: (diffIndex: number) => void): [T];
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { findListDiffIndex } from "../utils/algorithmUtil";
|
||||
export default function useDiffItem(data, getKey, onDiff) {
|
||||
const [prevData, setPrevData] = React.useState(data);
|
||||
const [diffItem, setDiffItem] = React.useState(null);
|
||||
React.useEffect(() => {
|
||||
const diff = findListDiffIndex(prevData || [], data || [], getKey);
|
||||
if (diff?.index !== undefined) {
|
||||
onDiff?.(diff.index);
|
||||
setDiffItem(data[diff.index]);
|
||||
}
|
||||
setPrevData(data);
|
||||
}, [data]);
|
||||
return [diffItem];
|
||||
}
|
||||
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
interface FireFoxDOMMouseScrollEvent {
|
||||
detail: number;
|
||||
preventDefault: VoidFunction;
|
||||
}
|
||||
export default function useFrameWheel(inVirtual: boolean, isScrollAtTop: boolean, isScrollAtBottom: boolean, isScrollAtLeft: boolean, isScrollAtRight: boolean, horizontalScroll: boolean,
|
||||
/***
|
||||
* Return `true` when you need to prevent default event
|
||||
*/
|
||||
onWheelDelta: (offset: number, horizontal: boolean) => void): [(e: WheelEvent) => void, (e: FireFoxDOMMouseScrollEvent) => void];
|
||||
export {};
|
||||
Generated
Vendored
+95
@@ -0,0 +1,95 @@
|
||||
import raf from "@rc-component/util/es/raf";
|
||||
import { useRef } from 'react';
|
||||
import isFF from "../utils/isFirefox";
|
||||
import useOriginScroll from "./useOriginScroll";
|
||||
export default function useFrameWheel(inVirtual, isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight, horizontalScroll,
|
||||
/***
|
||||
* Return `true` when you need to prevent default event
|
||||
*/
|
||||
onWheelDelta) {
|
||||
const offsetRef = useRef(0);
|
||||
const nextFrameRef = useRef(null);
|
||||
|
||||
// Firefox patch
|
||||
const wheelValueRef = useRef(null);
|
||||
const isMouseScrollRef = useRef(false);
|
||||
|
||||
// Scroll status sync
|
||||
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight);
|
||||
function onWheelY(e, deltaY) {
|
||||
raf.cancel(nextFrameRef.current);
|
||||
|
||||
// Do nothing when scroll at the edge, Skip check when is in scroll
|
||||
if (originScroll(false, deltaY)) return;
|
||||
|
||||
// Skip if nest List has handled this event
|
||||
const event = e;
|
||||
if (!event._virtualHandled) {
|
||||
event._virtualHandled = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
offsetRef.current += deltaY;
|
||||
wheelValueRef.current = deltaY;
|
||||
|
||||
// Proxy of scroll events
|
||||
if (!isFF) {
|
||||
event.preventDefault();
|
||||
}
|
||||
nextFrameRef.current = raf(() => {
|
||||
// Patch a multiple for Firefox to fix wheel number too small
|
||||
// ref: https://github.com/ant-design/ant-design/issues/26372#issuecomment-679460266
|
||||
const patchMultiple = isMouseScrollRef.current ? 10 : 1;
|
||||
onWheelDelta(offsetRef.current * patchMultiple, false);
|
||||
offsetRef.current = 0;
|
||||
});
|
||||
}
|
||||
function onWheelX(event, deltaX) {
|
||||
onWheelDelta(deltaX, true);
|
||||
if (!isFF) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for which direction does wheel do. `sx` means `shift + wheel`
|
||||
const wheelDirectionRef = useRef(null);
|
||||
const wheelDirectionCleanRef = useRef(null);
|
||||
function onWheel(event) {
|
||||
if (!inVirtual) return;
|
||||
|
||||
// Wait for 2 frame to clean direction
|
||||
raf.cancel(wheelDirectionCleanRef.current);
|
||||
wheelDirectionCleanRef.current = raf(() => {
|
||||
wheelDirectionRef.current = null;
|
||||
}, 2);
|
||||
const {
|
||||
deltaX,
|
||||
deltaY,
|
||||
shiftKey
|
||||
} = event;
|
||||
let mergedDeltaX = deltaX;
|
||||
let mergedDeltaY = deltaY;
|
||||
if (wheelDirectionRef.current === 'sx' || !wheelDirectionRef.current && (shiftKey || false) && deltaY && !deltaX) {
|
||||
mergedDeltaX = deltaY;
|
||||
mergedDeltaY = 0;
|
||||
wheelDirectionRef.current = 'sx';
|
||||
}
|
||||
const absX = Math.abs(mergedDeltaX);
|
||||
const absY = Math.abs(mergedDeltaY);
|
||||
if (wheelDirectionRef.current === null) {
|
||||
wheelDirectionRef.current = horizontalScroll && absX > absY ? 'x' : 'y';
|
||||
}
|
||||
if (wheelDirectionRef.current === 'y') {
|
||||
onWheelY(event, mergedDeltaY);
|
||||
} else {
|
||||
onWheelX(event, mergedDeltaX);
|
||||
}
|
||||
}
|
||||
|
||||
// A patch for firefox
|
||||
function onFireFoxScroll(event) {
|
||||
if (!inVirtual) return;
|
||||
isMouseScrollRef.current = event.detail === wheelValueRef.current;
|
||||
}
|
||||
return [onWheel, onFireFoxScroll];
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import type CacheMap from '../utils/CacheMap';
|
||||
import type { GetKey, GetSize } from '../interface';
|
||||
/**
|
||||
* Size info need loop query for the `heights` which will has the perf issue.
|
||||
* Let cache result for each render phase.
|
||||
*/
|
||||
export declare function useGetSize<T>(mergedData: T[], getKey: GetKey<T>, heights: CacheMap, itemHeight: number): GetSize;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Size info need loop query for the `heights` which will has the perf issue.
|
||||
* Let cache result for each render phase.
|
||||
*/
|
||||
export function useGetSize(mergedData, getKey, heights, itemHeight) {
|
||||
const [key2Index, bottomList] = React.useMemo(() => [new Map(), []], [mergedData, heights.id, itemHeight]);
|
||||
const getSize = (startKey, endKey = startKey) => {
|
||||
// Get from cache first
|
||||
let startIndex = key2Index.get(startKey);
|
||||
let endIndex = key2Index.get(endKey);
|
||||
|
||||
// Loop to fill the cache
|
||||
if (startIndex === undefined || endIndex === undefined) {
|
||||
const dataLen = mergedData.length;
|
||||
for (let i = bottomList.length; i < dataLen; i += 1) {
|
||||
const item = mergedData[i];
|
||||
const key = getKey(item);
|
||||
key2Index.set(key, i);
|
||||
const cacheHeight = heights.get(key) ?? itemHeight;
|
||||
bottomList[i] = (bottomList[i - 1] || 0) + cacheHeight;
|
||||
if (key === startKey) {
|
||||
startIndex = i;
|
||||
}
|
||||
if (key === endKey) {
|
||||
endIndex = i;
|
||||
}
|
||||
if (startIndex !== undefined && endIndex !== undefined) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
top: bottomList[startIndex - 1] || 0,
|
||||
bottom: bottomList[endIndex]
|
||||
};
|
||||
};
|
||||
return getSize;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import type { GetKey } from '../interface';
|
||||
import CacheMap from '../utils/CacheMap';
|
||||
export default function useHeights<T>(getKey: GetKey<T>, onItemAdd?: (item: T) => void, onItemRemove?: (item: T) => void): [
|
||||
setInstanceRef: (item: T, instance: HTMLElement) => void,
|
||||
collectHeight: (sync?: boolean) => void,
|
||||
cacheMap: CacheMap,
|
||||
updatedMark: number
|
||||
];
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import CacheMap from "../utils/CacheMap";
|
||||
function parseNumber(value) {
|
||||
const num = parseFloat(value);
|
||||
return isNaN(num) ? 0 : num;
|
||||
}
|
||||
export default function useHeights(getKey, onItemAdd, onItemRemove) {
|
||||
const [updatedMark, setUpdatedMark] = React.useState(0);
|
||||
const instanceRef = useRef(new Map());
|
||||
const heightsRef = useRef(new CacheMap());
|
||||
const promiseIdRef = useRef(0);
|
||||
function cancelRaf() {
|
||||
promiseIdRef.current += 1;
|
||||
}
|
||||
function collectHeight(sync = false) {
|
||||
cancelRaf();
|
||||
const doCollect = () => {
|
||||
let changed = false;
|
||||
instanceRef.current.forEach((element, key) => {
|
||||
if (element && element.offsetParent) {
|
||||
const {
|
||||
offsetHeight
|
||||
} = element;
|
||||
const {
|
||||
marginTop,
|
||||
marginBottom
|
||||
} = getComputedStyle(element);
|
||||
const marginTopNum = parseNumber(marginTop);
|
||||
const marginBottomNum = parseNumber(marginBottom);
|
||||
const totalHeight = offsetHeight + marginTopNum + marginBottomNum;
|
||||
if (heightsRef.current.get(key) !== totalHeight) {
|
||||
heightsRef.current.set(key, totalHeight);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Always trigger update mark to tell parent that should re-calculate heights when resized
|
||||
if (changed) {
|
||||
setUpdatedMark(c => c + 1);
|
||||
}
|
||||
};
|
||||
if (sync) {
|
||||
doCollect();
|
||||
} else {
|
||||
promiseIdRef.current += 1;
|
||||
const id = promiseIdRef.current;
|
||||
Promise.resolve().then(() => {
|
||||
if (id === promiseIdRef.current) {
|
||||
doCollect();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function setInstanceRef(item, instance) {
|
||||
const key = getKey(item);
|
||||
const origin = instanceRef.current.get(key);
|
||||
if (instance) {
|
||||
instanceRef.current.set(key, instance);
|
||||
collectHeight();
|
||||
} else {
|
||||
instanceRef.current.delete(key);
|
||||
}
|
||||
|
||||
// Instance changed
|
||||
if (!origin !== !instance) {
|
||||
if (instance) {
|
||||
onItemAdd?.(item);
|
||||
} else {
|
||||
onItemRemove?.(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
return cancelRaf;
|
||||
}, []);
|
||||
return [setInstanceRef, collectHeight, heightsRef.current, updatedMark];
|
||||
}
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import type * as React from 'react';
|
||||
export default function useMobileTouchMove(inVirtual: boolean, listRef: React.RefObject<HTMLDivElement>, callback: (isHorizontal: boolean, offset: number, smoothOffset: boolean, e?: TouchEvent) => boolean): void;
|
||||
Generated
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
|
||||
import { useRef } from 'react';
|
||||
const SMOOTH_PTG = 14 / 15;
|
||||
export default function useMobileTouchMove(inVirtual, listRef, callback) {
|
||||
const touchedRef = useRef(false);
|
||||
const touchXRef = useRef(0);
|
||||
const touchYRef = useRef(0);
|
||||
const elementRef = useRef(null);
|
||||
|
||||
// Smooth scroll
|
||||
const intervalRef = useRef(null);
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
let cleanUpEvents;
|
||||
const onTouchMove = e => {
|
||||
if (touchedRef.current) {
|
||||
const currentX = Math.ceil(e.touches[0].pageX);
|
||||
const currentY = Math.ceil(e.touches[0].pageY);
|
||||
let offsetX = touchXRef.current - currentX;
|
||||
let offsetY = touchYRef.current - currentY;
|
||||
const isHorizontal = Math.abs(offsetX) > Math.abs(offsetY);
|
||||
if (isHorizontal) {
|
||||
touchXRef.current = currentX;
|
||||
} else {
|
||||
touchYRef.current = currentY;
|
||||
}
|
||||
const scrollHandled = callback(isHorizontal, isHorizontal ? offsetX : offsetY, false, e);
|
||||
if (scrollHandled) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Smooth interval
|
||||
clearInterval(intervalRef.current);
|
||||
if (scrollHandled) {
|
||||
intervalRef.current = setInterval(() => {
|
||||
if (isHorizontal) {
|
||||
offsetX *= SMOOTH_PTG;
|
||||
} else {
|
||||
offsetY *= SMOOTH_PTG;
|
||||
}
|
||||
const offset = Math.floor(isHorizontal ? offsetX : offsetY);
|
||||
if (!callback(isHorizontal, offset, true) || Math.abs(offset) <= 0.1) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
}, 16);
|
||||
}
|
||||
}
|
||||
};
|
||||
const onTouchEnd = () => {
|
||||
touchedRef.current = false;
|
||||
cleanUpEvents();
|
||||
};
|
||||
const onTouchStart = e => {
|
||||
cleanUpEvents();
|
||||
if (e.touches.length === 1 && !touchedRef.current) {
|
||||
touchedRef.current = true;
|
||||
touchXRef.current = Math.ceil(e.touches[0].pageX);
|
||||
touchYRef.current = Math.ceil(e.touches[0].pageY);
|
||||
elementRef.current = e.target;
|
||||
elementRef.current.addEventListener('touchmove', onTouchMove, {
|
||||
passive: false
|
||||
});
|
||||
elementRef.current.addEventListener('touchend', onTouchEnd, {
|
||||
passive: true
|
||||
});
|
||||
}
|
||||
};
|
||||
cleanUpEvents = () => {
|
||||
if (elementRef.current) {
|
||||
elementRef.current.removeEventListener('touchmove', onTouchMove);
|
||||
elementRef.current.removeEventListener('touchend', onTouchEnd);
|
||||
}
|
||||
};
|
||||
useLayoutEffect(() => {
|
||||
if (inVirtual) {
|
||||
listRef.current.addEventListener('touchstart', onTouchStart, {
|
||||
passive: true
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
listRef.current?.removeEventListener('touchstart', onTouchStart);
|
||||
cleanUpEvents();
|
||||
clearInterval(intervalRef.current);
|
||||
};
|
||||
}, [inVirtual]);
|
||||
}
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
declare const _default: (isScrollAtTop: boolean, isScrollAtBottom: boolean, isScrollAtLeft: boolean, isScrollAtRight: boolean) => (isHorizontal: boolean, delta: number, smoothOffset?: boolean) => boolean;
|
||||
export default _default;
|
||||
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
import { useRef } from 'react';
|
||||
export default ((isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight) => {
|
||||
// Do lock for a wheel when scrolling
|
||||
const lockRef = useRef(false);
|
||||
const lockTimeoutRef = useRef(null);
|
||||
function lockScroll() {
|
||||
clearTimeout(lockTimeoutRef.current);
|
||||
lockRef.current = true;
|
||||
lockTimeoutRef.current = setTimeout(() => {
|
||||
lockRef.current = false;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// Pass to ref since global add is in closure
|
||||
const scrollPingRef = useRef({
|
||||
top: isScrollAtTop,
|
||||
bottom: isScrollAtBottom,
|
||||
left: isScrollAtLeft,
|
||||
right: isScrollAtRight
|
||||
});
|
||||
scrollPingRef.current.top = isScrollAtTop;
|
||||
scrollPingRef.current.bottom = isScrollAtBottom;
|
||||
scrollPingRef.current.left = isScrollAtLeft;
|
||||
scrollPingRef.current.right = isScrollAtRight;
|
||||
return (isHorizontal, delta, smoothOffset = false) => {
|
||||
const originScroll = isHorizontal ?
|
||||
// Pass origin wheel when on the left
|
||||
delta < 0 && scrollPingRef.current.left ||
|
||||
// Pass origin wheel when on the right
|
||||
delta > 0 && scrollPingRef.current.right // Pass origin wheel when on the top
|
||||
: delta < 0 && scrollPingRef.current.top ||
|
||||
// Pass origin wheel when on the bottom
|
||||
delta > 0 && scrollPingRef.current.bottom;
|
||||
if (smoothOffset && originScroll) {
|
||||
// No need lock anymore when it's smooth offset from touchMove interval
|
||||
clearTimeout(lockTimeoutRef.current);
|
||||
lockRef.current = false;
|
||||
} else if (!originScroll || lockRef.current) {
|
||||
lockScroll();
|
||||
}
|
||||
return !lockRef.current && originScroll;
|
||||
};
|
||||
});
|
||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import * as React from 'react';
|
||||
export declare function getPageXY(e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent, horizontal: boolean): number;
|
||||
export default function useScrollDrag(inVirtual: boolean, componentRef: React.RefObject<HTMLElement>, onScrollOffset: (offset: number) => void): void;
|
||||
Generated
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
import raf from "@rc-component/util/es/raf";
|
||||
import * as React from 'react';
|
||||
function smoothScrollOffset(offset) {
|
||||
return Math.floor(offset ** 0.5);
|
||||
}
|
||||
export function getPageXY(e, horizontal) {
|
||||
const obj = 'touches' in e ? e.touches[0] : e;
|
||||
return obj[horizontal ? 'pageX' : 'pageY'] - window[horizontal ? 'scrollX' : 'scrollY'];
|
||||
}
|
||||
export default function useScrollDrag(inVirtual, componentRef, onScrollOffset) {
|
||||
React.useEffect(() => {
|
||||
const ele = componentRef.current;
|
||||
if (inVirtual && ele) {
|
||||
let mouseDownLock = false;
|
||||
let rafId;
|
||||
let offset;
|
||||
const stopScroll = () => {
|
||||
raf.cancel(rafId);
|
||||
};
|
||||
const continueScroll = () => {
|
||||
stopScroll();
|
||||
rafId = raf(() => {
|
||||
onScrollOffset(offset);
|
||||
continueScroll();
|
||||
});
|
||||
};
|
||||
const clearDragState = () => {
|
||||
mouseDownLock = false;
|
||||
stopScroll();
|
||||
};
|
||||
const onMouseDown = e => {
|
||||
// Skip if element set draggable
|
||||
if (e.target.draggable || e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
// Skip if nest List has handled this event
|
||||
const event = e;
|
||||
if (!event._virtualHandled) {
|
||||
event._virtualHandled = true;
|
||||
mouseDownLock = true;
|
||||
}
|
||||
};
|
||||
const onMouseMove = e => {
|
||||
if (mouseDownLock) {
|
||||
const mouseY = getPageXY(e, false);
|
||||
const {
|
||||
top,
|
||||
bottom
|
||||
} = ele.getBoundingClientRect();
|
||||
if (mouseY <= top) {
|
||||
const diff = top - mouseY;
|
||||
offset = -smoothScrollOffset(diff);
|
||||
continueScroll();
|
||||
} else if (mouseY >= bottom) {
|
||||
const diff = mouseY - bottom;
|
||||
offset = smoothScrollOffset(diff);
|
||||
continueScroll();
|
||||
} else {
|
||||
stopScroll();
|
||||
}
|
||||
}
|
||||
};
|
||||
ele.addEventListener('mousedown', onMouseDown);
|
||||
ele.ownerDocument.addEventListener('mouseup', clearDragState);
|
||||
ele.ownerDocument.addEventListener('mousemove', onMouseMove);
|
||||
ele.ownerDocument.addEventListener('dragend', clearDragState);
|
||||
return () => {
|
||||
ele.removeEventListener('mousedown', onMouseDown);
|
||||
ele.ownerDocument.removeEventListener('mouseup', clearDragState);
|
||||
ele.ownerDocument.removeEventListener('mousemove', onMouseMove);
|
||||
ele.ownerDocument.removeEventListener('dragend', clearDragState);
|
||||
stopScroll();
|
||||
};
|
||||
}
|
||||
}, [inVirtual]);
|
||||
}
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import type { GetKey } from '../interface';
|
||||
import type CacheMap from '../utils/CacheMap';
|
||||
export type ScrollAlign = 'top' | 'bottom' | 'auto';
|
||||
export type ScrollPos = {
|
||||
left?: number;
|
||||
top?: number;
|
||||
};
|
||||
export type ScrollTarget = {
|
||||
index: number;
|
||||
align?: ScrollAlign;
|
||||
offset?: number;
|
||||
} | {
|
||||
key: React.Key;
|
||||
align?: ScrollAlign;
|
||||
offset?: number;
|
||||
};
|
||||
export default function useScrollTo<T>(containerRef: React.RefObject<HTMLDivElement>, data: T[], heights: CacheMap, itemHeight: number, getKey: GetKey<T>, collectHeight: () => void, syncScrollTop: (newTop: number) => void, triggerFlash: () => void): (arg: number | ScrollTarget) => void;
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import * as React from 'react';
|
||||
import raf from "@rc-component/util/es/raf";
|
||||
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
|
||||
import { warning } from '@rc-component/util';
|
||||
const MAX_TIMES = 10;
|
||||
export default function useScrollTo(containerRef, data, heights, itemHeight, getKey, collectHeight, syncScrollTop, triggerFlash) {
|
||||
const scrollRef = React.useRef();
|
||||
const [syncState, setSyncState] = React.useState(null);
|
||||
|
||||
// ========================== Sync Scroll ==========================
|
||||
useLayoutEffect(() => {
|
||||
if (syncState && syncState.times < MAX_TIMES) {
|
||||
// Never reach
|
||||
if (!containerRef.current) {
|
||||
setSyncState(ori => ({
|
||||
...ori
|
||||
}));
|
||||
return;
|
||||
}
|
||||
collectHeight();
|
||||
const {
|
||||
targetAlign,
|
||||
originAlign,
|
||||
index,
|
||||
offset
|
||||
} = syncState;
|
||||
const height = containerRef.current.clientHeight;
|
||||
let needCollectHeight = false;
|
||||
let newTargetAlign = targetAlign;
|
||||
let targetTop = null;
|
||||
|
||||
// Go to next frame if height not exist
|
||||
if (height) {
|
||||
const mergedAlign = targetAlign || originAlign;
|
||||
|
||||
// Get top & bottom
|
||||
let stackTop = 0;
|
||||
let itemTop = 0;
|
||||
let itemBottom = 0;
|
||||
const maxLen = Math.min(data.length - 1, index);
|
||||
for (let i = 0; i <= maxLen; i += 1) {
|
||||
const key = getKey(data[i]);
|
||||
itemTop = stackTop;
|
||||
const cacheHeight = heights.get(key);
|
||||
itemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);
|
||||
stackTop = itemBottom;
|
||||
}
|
||||
|
||||
// Check if need sync height (visible range has item not record height)
|
||||
let leftHeight = mergedAlign === 'top' ? offset : height - offset;
|
||||
for (let i = maxLen; i >= 0; i -= 1) {
|
||||
const key = getKey(data[i]);
|
||||
const cacheHeight = heights.get(key);
|
||||
if (cacheHeight === undefined) {
|
||||
needCollectHeight = true;
|
||||
break;
|
||||
}
|
||||
leftHeight -= cacheHeight;
|
||||
if (leftHeight <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to
|
||||
switch (mergedAlign) {
|
||||
case 'top':
|
||||
targetTop = itemTop - offset;
|
||||
break;
|
||||
case 'bottom':
|
||||
targetTop = itemBottom - height + offset;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
const {
|
||||
scrollTop
|
||||
} = containerRef.current;
|
||||
const scrollBottom = scrollTop + height;
|
||||
if (itemTop < scrollTop) {
|
||||
newTargetAlign = 'top';
|
||||
} else if (itemBottom > scrollBottom) {
|
||||
newTargetAlign = 'bottom';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetTop !== null) {
|
||||
syncScrollTop(targetTop);
|
||||
}
|
||||
|
||||
// One more time for sync
|
||||
if (targetTop !== syncState.lastTop) {
|
||||
needCollectHeight = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger next effect
|
||||
if (needCollectHeight) {
|
||||
setSyncState({
|
||||
...syncState,
|
||||
times: syncState.times + 1,
|
||||
targetAlign: newTargetAlign,
|
||||
lastTop: targetTop
|
||||
});
|
||||
}
|
||||
} else if (process.env.NODE_ENV !== 'production' && syncState?.times === MAX_TIMES) {
|
||||
warning(false, 'Seems `scrollTo` with `rc-virtual-list` reach the max limitation. Please fire issue for us. Thanks.');
|
||||
}
|
||||
}, [syncState, containerRef.current]);
|
||||
|
||||
// =========================== Scroll To ===========================
|
||||
return arg => {
|
||||
// When not argument provided, we think dev may want to show the scrollbar
|
||||
if (arg === null || arg === undefined) {
|
||||
triggerFlash();
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal scroll logic
|
||||
raf.cancel(scrollRef.current);
|
||||
if (typeof arg === 'number') {
|
||||
syncScrollTop(arg);
|
||||
} else if (arg && typeof arg === 'object') {
|
||||
let index;
|
||||
const {
|
||||
align
|
||||
} = arg;
|
||||
if ('index' in arg) {
|
||||
({
|
||||
index
|
||||
} = arg);
|
||||
} else {
|
||||
index = data.findIndex(item => getKey(item) === arg.key);
|
||||
}
|
||||
const {
|
||||
offset = 0
|
||||
} = arg;
|
||||
setSyncState({
|
||||
times: 0,
|
||||
index,
|
||||
offset,
|
||||
originAlign: align
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user