This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
@@ -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[];
@@ -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);
});
}
@@ -0,0 +1,2 @@
import type { GetKey } from '../interface';
export default function useDiffItem<T>(data: T[], getKey: GetKey<T>, onDiff?: (diffIndex: number) => void): [T];
@@ -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];
}
@@ -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 {};
@@ -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];
}
@@ -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;
@@ -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;
}
@@ -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
];
@@ -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];
}
@@ -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;
@@ -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]);
}
@@ -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;
@@ -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;
};
});
@@ -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;
@@ -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]);
}
@@ -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;
@@ -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
});
}
};
}