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,2 @@
import * as React from 'react';
export declare const WheelLockContext: React.Context<(lock: boolean) => void>;
@@ -0,0 +1,9 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.WheelLockContext = void 0;
var React = _interopRequireWildcard(require("react"));
const WheelLockContext = exports.WheelLockContext = /*#__PURE__*/React.createContext(() => {});
@@ -0,0 +1,21 @@
import * as React from 'react';
export type InnerProps = Pick<React.HTMLAttributes<HTMLDivElement>, 'role' | 'id'>;
interface FillerProps {
prefixCls?: string;
/** Virtual filler height. Should be `count * itemMinHeight` */
height: number;
/** Set offset of visible items. Should be the top of start item position */
offsetY?: number;
offsetX?: number;
scrollWidth?: number;
children: React.ReactNode;
onInnerResize?: () => void;
innerProps?: InnerProps;
rtl: boolean;
extra?: React.ReactNode;
}
/**
* Fill component to provided the scroll content real height.
*/
declare const Filler: React.ForwardRefExoticComponent<FillerProps & React.RefAttributes<HTMLDivElement>>;
export default Filler;
+68
View File
@@ -0,0 +1,68 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var React = _interopRequireWildcard(require("react"));
var _resizeObserver = _interopRequireDefault(require("@rc-component/resize-observer"));
var _clsx = require("clsx");
/**
* Fill component to provided the scroll content real height.
*/
const Filler = /*#__PURE__*/React.forwardRef(({
height,
offsetY,
offsetX,
children,
prefixCls,
onInnerResize,
innerProps,
rtl,
extra
}, ref) => {
let outerStyle = {};
let innerStyle = {
display: 'flex',
flexDirection: 'column'
};
if (offsetY !== undefined) {
// Not set `width` since this will break `sticky: right`
outerStyle = {
height,
position: 'relative',
overflow: 'hidden'
};
innerStyle = {
...innerStyle,
transform: `translateY(${offsetY}px)`,
[rtl ? 'marginRight' : 'marginLeft']: -offsetX,
position: 'absolute',
left: 0,
right: 0,
top: 0
};
}
return /*#__PURE__*/React.createElement("div", {
style: outerStyle
}, /*#__PURE__*/React.createElement(_resizeObserver.default, {
onResize: ({
offsetHeight
}) => {
if (offsetHeight && onInnerResize) {
onInnerResize();
}
}
}, /*#__PURE__*/React.createElement("div", (0, _extends2.default)({
style: innerStyle,
className: (0, _clsx.clsx)({
[`${prefixCls}-holder-inner`]: prefixCls
}),
ref: ref
}, innerProps), children, extra)));
});
Filler.displayName = 'Filler';
var _default = exports.default = Filler;
@@ -0,0 +1,6 @@
import * as React from 'react';
export interface ItemProps {
children: React.ReactElement;
setRef: (element: HTMLElement) => void;
}
export declare function Item({ children, setRef }: ItemProps): React.ReactElement<any, string | React.JSXElementConstructor<any>>;
+19
View File
@@ -0,0 +1,19 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Item = Item;
var React = _interopRequireWildcard(require("react"));
function Item({
children,
setRef
}) {
const refFunc = React.useCallback(node => {
setRef(node);
}, []);
return /*#__PURE__*/React.cloneElement(children, {
ref: refFunc
});
}
+61
View File
@@ -0,0 +1,61 @@
import * as React from 'react';
import type { InnerProps } from './Filler';
import type { ScrollPos, ScrollTarget } from './hooks/useScrollTo';
import type { ExtraRenderInfo, RenderFunc } from './interface';
import type { ScrollBarDirectionType } from './ScrollBar';
export interface ScrollInfo {
x: number;
y: number;
}
export type ScrollConfig = ScrollTarget | ScrollPos;
export type ScrollTo = (arg?: number | ScrollConfig | null) => void;
export type ListRef = {
nativeElement: HTMLDivElement;
scrollTo: ScrollTo;
getScrollInfo: () => ScrollInfo;
};
export interface ListProps<T> extends Omit<React.HTMLAttributes<any>, 'children'> {
prefixCls?: string;
children: RenderFunc<T>;
data: T[];
height?: number;
itemHeight?: number;
/** If not match virtual scroll condition, Set List still use height of container. */
fullHeight?: boolean;
itemKey: React.Key | ((item: T) => React.Key);
component?: string | React.FC<any> | React.ComponentClass<any>;
/** Set `false` will always use real scroll instead of virtual one */
virtual?: boolean;
direction?: ScrollBarDirectionType;
/**
* By default `scrollWidth` is same as container.
* When set this, it will show the horizontal scrollbar and
* `scrollWidth` will be used as the real width instead of container width.
* When set, `virtual` will always be enabled.
*/
scrollWidth?: number;
styles?: {
horizontalScrollBar?: React.CSSProperties;
horizontalScrollBarThumb?: React.CSSProperties;
verticalScrollBar?: React.CSSProperties;
verticalScrollBarThumb?: React.CSSProperties;
};
showScrollBar?: boolean | 'optional';
onScroll?: React.UIEventHandler<HTMLElement>;
/**
* Given the virtual offset value.
* It's the logic offset from start position.
*/
onVirtualScroll?: (info: ScrollInfo) => void;
/** Trigger when render list item changed */
onVisibleChange?: (visibleList: T[], fullList: T[]) => void;
/** Inject to inner container props. Only use when you need pass aria related data */
innerProps?: InnerProps;
/** Render extra content into Filler */
extraRender?: (info: ExtraRenderInfo) => React.ReactNode;
}
export declare function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>): React.JSX.Element;
declare const _default: <Item = any>(props: ListProps<Item> & {
ref?: React.Ref<ListRef>;
}) => React.ReactElement;
export default _default;
+521
View File
@@ -0,0 +1,521 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.RawList = RawList;
exports.default = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _clsx = require("clsx");
var _resizeObserver = _interopRequireDefault(require("@rc-component/resize-observer"));
var _util = require("@rc-component/util");
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var _react = _interopRequireWildcard(require("react"));
var React = _react;
var _reactDom = require("react-dom");
var _Filler = _interopRequireDefault(require("./Filler"));
var _useChildren = _interopRequireDefault(require("./hooks/useChildren"));
var _useDiffItem = _interopRequireDefault(require("./hooks/useDiffItem"));
var _useFrameWheel = _interopRequireDefault(require("./hooks/useFrameWheel"));
var _useGetSize = require("./hooks/useGetSize");
var _useHeights = _interopRequireDefault(require("./hooks/useHeights"));
var _useMobileTouchMove = _interopRequireDefault(require("./hooks/useMobileTouchMove"));
var _useOriginScroll = _interopRequireDefault(require("./hooks/useOriginScroll"));
var _useScrollDrag = _interopRequireDefault(require("./hooks/useScrollDrag"));
var _useScrollTo = _interopRequireDefault(require("./hooks/useScrollTo"));
var _ScrollBar = _interopRequireDefault(require("./ScrollBar"));
var _scrollbarUtil = require("./utils/scrollbarUtil");
const EMPTY_DATA = [];
const ScrollStyle = {
overflowY: 'auto',
overflowAnchor: 'none'
};
function RawList(props, ref) {
const {
prefixCls = 'rc-virtual-list',
className,
height,
itemHeight,
fullHeight = true,
style,
data,
children,
itemKey,
virtual,
direction,
scrollWidth,
component: Component = 'div',
onScroll,
onVirtualScroll,
onVisibleChange,
innerProps,
extraRender,
styles,
showScrollBar = 'optional',
...restProps
} = props;
// =============================== Item Key ===============================
const getKey = React.useCallback(item => {
if (typeof itemKey === 'function') {
return itemKey(item);
}
return item?.[itemKey];
}, [itemKey]);
// ================================ Height ================================
const [setInstanceRef, collectHeight, heights, heightUpdatedMark] = (0, _useHeights.default)(getKey, null, null);
// ================================= MISC =================================
const useVirtual = !!(virtual !== false && height && itemHeight);
const containerHeight = React.useMemo(() => Object.values(heights.maps).reduce((total, curr) => total + curr, 0), [heights.id, heights.maps]);
const inVirtual = useVirtual && data && (Math.max(itemHeight * data.length, containerHeight) > height || !!scrollWidth);
const isRTL = direction === 'rtl';
const mergedClassName = (0, _clsx.clsx)(prefixCls, {
[`${prefixCls}-rtl`]: isRTL
}, className);
const mergedData = data || EMPTY_DATA;
const componentRef = (0, _react.useRef)();
const fillerInnerRef = (0, _react.useRef)();
const containerRef = (0, _react.useRef)();
// =============================== Item Key ===============================
const [offsetTop, setOffsetTop] = (0, _react.useState)(0);
const [offsetLeft, setOffsetLeft] = (0, _react.useState)(0);
const [scrollMoving, setScrollMoving] = (0, _react.useState)(false);
const onScrollbarStartMove = () => {
setScrollMoving(true);
};
const onScrollbarStopMove = () => {
setScrollMoving(false);
};
const sharedConfig = {
getKey
};
// ================================ Scroll ================================
function syncScrollTop(newTop) {
setOffsetTop(origin => {
let value;
if (typeof newTop === 'function') {
value = newTop(origin);
} else {
value = newTop;
}
const alignedTop = keepInRange(value);
componentRef.current.scrollTop = alignedTop;
return alignedTop;
});
}
// ================================ Legacy ================================
// Put ref here since the range is generate by follow
const rangeRef = (0, _react.useRef)({
start: 0,
end: mergedData.length
});
const diffItemRef = (0, _react.useRef)();
const [diffItem] = (0, _useDiffItem.default)(mergedData, getKey);
diffItemRef.current = diffItem;
// ========================== Visible Calculation =========================
const {
scrollHeight,
start,
end,
offset: fillerOffset
} = React.useMemo(() => {
if (!useVirtual) {
return {
scrollHeight: undefined,
start: 0,
end: mergedData.length - 1,
offset: undefined
};
}
// Always use virtual scroll bar in avoid shaking
if (!inVirtual) {
return {
scrollHeight: fillerInnerRef.current?.offsetHeight || 0,
start: 0,
end: mergedData.length - 1,
offset: undefined
};
}
let itemTop = 0;
let startIndex;
let startOffset;
let endIndex;
const dataLen = mergedData.length;
for (let i = 0; i < dataLen; i += 1) {
const item = mergedData[i];
const key = getKey(item);
const cacheHeight = heights.get(key);
const currentItemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);
// Check item top in the range
if (currentItemBottom >= offsetTop && startIndex === undefined) {
startIndex = i;
startOffset = itemTop;
}
// Check item bottom in the range. We will render additional one item for motion usage
if (currentItemBottom > offsetTop + height && endIndex === undefined) {
endIndex = i;
}
itemTop = currentItemBottom;
}
// When scrollTop at the end but data cut to small count will reach this
if (startIndex === undefined) {
startIndex = 0;
startOffset = 0;
endIndex = Math.ceil(height / itemHeight);
}
if (endIndex === undefined) {
endIndex = mergedData.length - 1;
}
// Give cache to improve scroll experience
endIndex = Math.min(endIndex + 1, mergedData.length - 1);
return {
scrollHeight: itemTop,
start: startIndex,
end: endIndex,
offset: startOffset
};
}, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height]);
rangeRef.current.start = start;
rangeRef.current.end = end;
// When scroll up, first visible item get real height may not same as `itemHeight`,
// Which will make scroll jump.
// Let's sync scroll top to avoid jump
React.useLayoutEffect(() => {
const changedRecord = heights.getRecord();
if (changedRecord.size === 1) {
const recordKey = Array.from(changedRecord.keys())[0];
const prevCacheHeight = changedRecord.get(recordKey);
// Quick switch data may cause `start` not in `mergedData` anymore
const startItem = mergedData[start];
if (startItem && prevCacheHeight === undefined) {
const startIndexKey = getKey(startItem);
if (startIndexKey === recordKey) {
const realStartHeight = heights.get(recordKey);
const diffHeight = realStartHeight - itemHeight;
syncScrollTop(ori => {
return ori + diffHeight;
});
}
}
}
heights.resetRecord();
}, [scrollHeight]);
// ================================= Size =================================
const [size, setSize] = React.useState({
width: 0,
height
});
const onHolderResize = sizeInfo => {
setSize({
width: sizeInfo.offsetWidth,
height: sizeInfo.offsetHeight
});
};
// Hack on scrollbar to enable flash call
const verticalScrollBarRef = (0, _react.useRef)();
const horizontalScrollBarRef = (0, _react.useRef)();
const horizontalScrollBarSpinSize = React.useMemo(() => (0, _scrollbarUtil.getSpinSize)(size.width, scrollWidth), [size.width, scrollWidth]);
const verticalScrollBarSpinSize = React.useMemo(() => (0, _scrollbarUtil.getSpinSize)(size.height, scrollHeight), [size.height, scrollHeight]);
// =============================== In Range ===============================
const maxScrollHeight = scrollHeight - height;
const maxScrollHeightRef = (0, _react.useRef)(maxScrollHeight);
maxScrollHeightRef.current = maxScrollHeight;
function keepInRange(newScrollTop) {
let newTop = newScrollTop;
if (!Number.isNaN(maxScrollHeightRef.current)) {
newTop = Math.min(newTop, maxScrollHeightRef.current);
}
newTop = Math.max(newTop, 0);
return newTop;
}
const isScrollAtTop = offsetTop <= 0;
const isScrollAtBottom = offsetTop >= maxScrollHeight;
const isScrollAtLeft = offsetLeft <= 0;
const isScrollAtRight = offsetLeft >= scrollWidth;
const originScroll = (0, _useOriginScroll.default)(isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight);
// ================================ Scroll ================================
const getVirtualScrollInfo = () => ({
x: isRTL ? -offsetLeft : offsetLeft,
y: offsetTop
});
const lastVirtualScrollInfoRef = (0, _react.useRef)(getVirtualScrollInfo());
const triggerScroll = (0, _util.useEvent)(params => {
if (onVirtualScroll) {
const nextInfo = {
...getVirtualScrollInfo(),
...params
};
// Trigger when offset changed
if (lastVirtualScrollInfoRef.current.x !== nextInfo.x || lastVirtualScrollInfoRef.current.y !== nextInfo.y) {
onVirtualScroll(nextInfo);
lastVirtualScrollInfoRef.current = nextInfo;
}
}
});
function onScrollBar(newScrollOffset, horizontal) {
const newOffset = newScrollOffset;
if (horizontal) {
(0, _reactDom.flushSync)(() => {
setOffsetLeft(newOffset);
});
triggerScroll();
} else {
syncScrollTop(newOffset);
}
}
// When data size reduce. It may trigger native scroll event back to fit scroll position
function onFallbackScroll(e) {
const {
scrollTop: newScrollTop
} = e.currentTarget;
if (newScrollTop !== offsetTop) {
syncScrollTop(newScrollTop);
}
// Trigger origin onScroll
onScroll?.(e);
triggerScroll();
}
const keepInHorizontalRange = nextOffsetLeft => {
let tmpOffsetLeft = nextOffsetLeft;
const max = !!scrollWidth ? scrollWidth - size.width : 0;
tmpOffsetLeft = Math.max(tmpOffsetLeft, 0);
tmpOffsetLeft = Math.min(tmpOffsetLeft, max);
return tmpOffsetLeft;
};
const onWheelDelta = (0, _util.useEvent)((offsetXY, fromHorizontal) => {
if (fromHorizontal) {
(0, _reactDom.flushSync)(() => {
setOffsetLeft(left => {
const nextOffsetLeft = left + (isRTL ? -offsetXY : offsetXY);
return keepInHorizontalRange(nextOffsetLeft);
});
});
triggerScroll();
} else {
syncScrollTop(top => {
const newTop = top + offsetXY;
return newTop;
});
}
});
// Since this added in global,should use ref to keep update
const [onRawWheel, onFireFoxScroll] = (0, _useFrameWheel.default)(useVirtual, isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight, !!scrollWidth, onWheelDelta);
// Mobile touch move
(0, _useMobileTouchMove.default)(useVirtual, componentRef, (isHorizontal, delta, smoothOffset, e) => {
const event = e;
if (originScroll(isHorizontal, delta, smoothOffset)) {
return false;
}
// Fix nest List trigger TouchMove event
if (!event || !event._virtualHandled) {
if (event) {
event._virtualHandled = true;
}
onRawWheel({
preventDefault() {},
deltaX: isHorizontal ? delta : 0,
deltaY: isHorizontal ? 0 : delta
});
return true;
}
return false;
});
// MouseDown drag for scroll
(0, _useScrollDrag.default)(inVirtual, componentRef, offset => {
syncScrollTop(top => top + offset);
});
(0, _useLayoutEffect.default)(() => {
// Firefox only
function onMozMousePixelScroll(e) {
// scrolling at top/bottom limit
const scrollingUpAtTop = isScrollAtTop && e.detail < 0;
const scrollingDownAtBottom = isScrollAtBottom && e.detail > 0;
if (useVirtual && !scrollingUpAtTop && !scrollingDownAtBottom) {
e.preventDefault();
}
}
const componentEle = componentRef.current;
componentEle.addEventListener('wheel', onRawWheel, {
passive: false
});
componentEle.addEventListener('DOMMouseScroll', onFireFoxScroll, {
passive: true
});
componentEle.addEventListener('MozMousePixelScroll', onMozMousePixelScroll, {
passive: false
});
return () => {
componentEle.removeEventListener('wheel', onRawWheel);
componentEle.removeEventListener('DOMMouseScroll', onFireFoxScroll);
componentEle.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll);
};
}, [useVirtual, isScrollAtTop, isScrollAtBottom]);
// Sync scroll left
(0, _useLayoutEffect.default)(() => {
if (scrollWidth) {
const newOffsetLeft = keepInHorizontalRange(offsetLeft);
setOffsetLeft(newOffsetLeft);
triggerScroll({
x: newOffsetLeft
});
}
}, [size.width, scrollWidth]);
// ================================= Ref ==================================
const delayHideScrollBar = () => {
verticalScrollBarRef.current?.delayHidden();
horizontalScrollBarRef.current?.delayHidden();
};
const scrollTo = (0, _useScrollTo.default)(componentRef, mergedData, heights, itemHeight, getKey, () => collectHeight(true), syncScrollTop, delayHideScrollBar);
React.useImperativeHandle(ref, () => ({
nativeElement: containerRef.current,
getScrollInfo: getVirtualScrollInfo,
scrollTo: config => {
function isPosScroll(arg) {
return arg && typeof arg === 'object' && ('left' in arg || 'top' in arg);
}
if (isPosScroll(config)) {
// Scroll X
if (config.left !== undefined) {
setOffsetLeft(keepInHorizontalRange(config.left));
}
// Scroll Y
scrollTo(config.top);
} else {
scrollTo(config);
}
}
}));
// ================================ Effect ================================
/** We need told outside that some list not rendered */
(0, _useLayoutEffect.default)(() => {
if (onVisibleChange) {
const renderList = mergedData.slice(start, end + 1);
onVisibleChange(renderList, mergedData);
}
}, [start, end, mergedData]);
// ================================ Extra =================================
const getSize = (0, _useGetSize.useGetSize)(mergedData, getKey, heights, itemHeight);
const extraContent = extraRender?.({
start,
end,
virtual: inVirtual,
offsetX: offsetLeft,
offsetY: fillerOffset,
rtl: isRTL,
getSize
});
// ================================ Render ================================
const listChildren = (0, _useChildren.default)(mergedData, start, end, scrollWidth, offsetLeft, setInstanceRef, children, sharedConfig);
let componentStyle = null;
if (height) {
componentStyle = {
[fullHeight ? 'height' : 'maxHeight']: height,
...ScrollStyle
};
if (useVirtual) {
componentStyle.overflowY = 'hidden';
if (scrollWidth) {
componentStyle.overflowX = 'hidden';
}
if (scrollMoving) {
componentStyle.pointerEvents = 'none';
}
}
}
const containerProps = {};
if (isRTL) {
containerProps.dir = 'rtl';
}
return /*#__PURE__*/React.createElement("div", (0, _extends2.default)({
ref: containerRef,
style: {
...style,
position: 'relative'
},
className: mergedClassName
}, containerProps, restProps), /*#__PURE__*/React.createElement(_resizeObserver.default, {
onResize: onHolderResize
}, /*#__PURE__*/React.createElement(Component, {
className: `${prefixCls}-holder`,
style: componentStyle,
ref: componentRef,
onScroll: onFallbackScroll,
onMouseEnter: delayHideScrollBar
}, /*#__PURE__*/React.createElement(_Filler.default, {
prefixCls: prefixCls,
height: scrollHeight,
offsetX: offsetLeft,
offsetY: fillerOffset,
scrollWidth: scrollWidth,
onInnerResize: collectHeight,
ref: fillerInnerRef,
innerProps: innerProps,
rtl: isRTL,
extra: extraContent
}, listChildren))), inVirtual && scrollHeight > height && /*#__PURE__*/React.createElement(_ScrollBar.default, {
ref: verticalScrollBarRef,
prefixCls: prefixCls,
scrollOffset: offsetTop,
scrollRange: scrollHeight,
rtl: isRTL,
onScroll: onScrollBar,
onStartMove: onScrollbarStartMove,
onStopMove: onScrollbarStopMove,
spinSize: verticalScrollBarSpinSize,
containerSize: size.height,
style: styles?.verticalScrollBar,
thumbStyle: styles?.verticalScrollBarThumb,
showScrollBar: showScrollBar
}), inVirtual && scrollWidth > size.width && /*#__PURE__*/React.createElement(_ScrollBar.default, {
ref: horizontalScrollBarRef,
prefixCls: prefixCls,
scrollOffset: offsetLeft,
scrollRange: scrollWidth,
rtl: isRTL,
onScroll: onScrollBar,
onStartMove: onScrollbarStartMove,
onStopMove: onScrollbarStopMove,
spinSize: horizontalScrollBarSpinSize,
containerSize: size.width,
horizontal: true,
style: styles?.horizontalScrollBar,
thumbStyle: styles?.horizontalScrollBarThumb,
showScrollBar: showScrollBar
}));
}
const List = /*#__PURE__*/React.forwardRef(RawList);
List.displayName = 'List';
var _default = exports.default = List;
@@ -0,0 +1,22 @@
import * as React from 'react';
export type ScrollBarDirectionType = 'ltr' | 'rtl';
export interface ScrollBarProps {
prefixCls: string;
scrollOffset: number;
scrollRange: number;
rtl: boolean;
onScroll: (scrollOffset: number, horizontal?: boolean) => void;
onStartMove: () => void;
onStopMove: () => void;
horizontal?: boolean;
style?: React.CSSProperties;
thumbStyle?: React.CSSProperties;
spinSize: number;
containerSize: number;
showScrollBar?: boolean | 'optional';
}
export interface ScrollBarRef {
delayHidden: () => void;
}
declare const ScrollBar: React.ForwardRefExoticComponent<ScrollBarProps & React.RefAttributes<ScrollBarRef>>;
export default ScrollBar;
@@ -0,0 +1,253 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _clsx = require("clsx");
var _raf = _interopRequireDefault(require("@rc-component/util/lib/raf"));
var React = _interopRequireWildcard(require("react"));
var _useScrollDrag = require("./hooks/useScrollDrag");
const ScrollBar = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls,
rtl,
scrollOffset,
scrollRange,
onStartMove,
onStopMove,
onScroll,
horizontal,
spinSize,
containerSize,
style,
thumbStyle: propsThumbStyle,
showScrollBar
} = props;
const [dragging, setDragging] = React.useState(false);
const [pageXY, setPageXY] = React.useState(null);
const [startTop, setStartTop] = React.useState(null);
const isLTR = !rtl;
// ========================= Refs =========================
const scrollbarRef = React.useRef();
const thumbRef = React.useRef();
// ======================= Visible ========================
const [visible, setVisible] = React.useState(showScrollBar);
const visibleTimeoutRef = React.useRef();
const delayHidden = () => {
if (showScrollBar === true || showScrollBar === false) return;
clearTimeout(visibleTimeoutRef.current);
setVisible(true);
visibleTimeoutRef.current = setTimeout(() => {
setVisible(false);
}, 3000);
};
// ======================== Range =========================
const enableScrollRange = scrollRange - containerSize || 0;
const enableOffsetRange = containerSize - spinSize || 0;
// ========================= Top ==========================
const top = React.useMemo(() => {
if (scrollOffset === 0 || enableScrollRange === 0) {
return 0;
}
const ptg = scrollOffset / enableScrollRange;
return ptg * enableOffsetRange;
}, [scrollOffset, enableScrollRange, enableOffsetRange]);
// ====================== Container =======================
const onContainerMouseDown = e => {
e.stopPropagation();
e.preventDefault();
};
// ======================== Thumb =========================
const stateRef = React.useRef({
top,
dragging,
pageY: pageXY,
startTop
});
stateRef.current = {
top,
dragging,
pageY: pageXY,
startTop
};
const onThumbMouseDown = e => {
setDragging(true);
setPageXY((0, _useScrollDrag.getPageXY)(e, horizontal));
setStartTop(stateRef.current.top);
onStartMove();
e.stopPropagation();
e.preventDefault();
};
// ======================== Effect ========================
// React make event as passive, but we need to preventDefault
// Add event on dom directly instead.
// ref: https://github.com/facebook/react/issues/9809
React.useEffect(() => {
const onScrollbarTouchStart = e => {
e.preventDefault();
};
const scrollbarEle = scrollbarRef.current;
const thumbEle = thumbRef.current;
scrollbarEle.addEventListener('touchstart', onScrollbarTouchStart, {
passive: false
});
thumbEle.addEventListener('touchstart', onThumbMouseDown, {
passive: false
});
return () => {
scrollbarEle.removeEventListener('touchstart', onScrollbarTouchStart);
thumbEle.removeEventListener('touchstart', onThumbMouseDown);
};
}, []);
// Pass to effect
const enableScrollRangeRef = React.useRef();
enableScrollRangeRef.current = enableScrollRange;
const enableOffsetRangeRef = React.useRef();
enableOffsetRangeRef.current = enableOffsetRange;
React.useEffect(() => {
if (dragging) {
let moveRafId;
const onMouseMove = e => {
const {
dragging: stateDragging,
pageY: statePageY,
startTop: stateStartTop
} = stateRef.current;
_raf.default.cancel(moveRafId);
const rect = scrollbarRef.current.getBoundingClientRect();
const scale = containerSize / (horizontal ? rect.width : rect.height);
if (stateDragging) {
const offset = ((0, _useScrollDrag.getPageXY)(e, horizontal) - statePageY) * scale;
let newTop = stateStartTop;
if (!isLTR && horizontal) {
newTop -= offset;
} else {
newTop += offset;
}
const tmpEnableScrollRange = enableScrollRangeRef.current;
const tmpEnableOffsetRange = enableOffsetRangeRef.current;
const ptg = tmpEnableOffsetRange ? newTop / tmpEnableOffsetRange : 0;
let newScrollTop = Math.ceil(ptg * tmpEnableScrollRange);
newScrollTop = Math.max(newScrollTop, 0);
newScrollTop = Math.min(newScrollTop, tmpEnableScrollRange);
moveRafId = (0, _raf.default)(() => {
onScroll(newScrollTop, horizontal);
});
}
};
const onMouseUp = () => {
setDragging(false);
onStopMove();
};
window.addEventListener('mousemove', onMouseMove, {
passive: true
});
window.addEventListener('touchmove', onMouseMove, {
passive: true
});
window.addEventListener('mouseup', onMouseUp, {
passive: true
});
window.addEventListener('touchend', onMouseUp, {
passive: true
});
return () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('touchmove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('touchend', onMouseUp);
_raf.default.cancel(moveRafId);
};
}
}, [dragging]);
React.useEffect(() => {
delayHidden();
return () => {
clearTimeout(visibleTimeoutRef.current);
};
}, [scrollOffset]);
// ====================== Imperative ======================
React.useImperativeHandle(ref, () => ({
delayHidden
}));
// ======================== Render ========================
const scrollbarPrefixCls = `${prefixCls}-scrollbar`;
const containerStyle = {
position: 'absolute',
visibility: visible ? null : 'hidden'
};
const thumbStyle = {
position: 'absolute',
borderRadius: 99,
background: 'var(--rc-virtual-list-scrollbar-bg, rgba(0, 0, 0, 0.5))',
cursor: 'pointer',
userSelect: 'none'
};
if (horizontal) {
Object.assign(containerStyle, {
height: 8,
left: 0,
right: 0,
bottom: 0
});
Object.assign(thumbStyle, {
height: '100%',
width: spinSize,
[isLTR ? 'left' : 'right']: top
});
} else {
Object.assign(containerStyle, {
width: 8,
top: 0,
bottom: 0,
[isLTR ? 'right' : 'left']: 0
});
Object.assign(thumbStyle, {
width: '100%',
height: spinSize,
top
});
}
return /*#__PURE__*/React.createElement("div", {
ref: scrollbarRef,
className: (0, _clsx.clsx)(scrollbarPrefixCls, {
[`${scrollbarPrefixCls}-horizontal`]: horizontal,
[`${scrollbarPrefixCls}-vertical`]: !horizontal,
[`${scrollbarPrefixCls}-visible`]: visible
}),
style: {
...containerStyle,
...style
},
onMouseDown: onContainerMouseDown,
onMouseMove: delayHidden
}, /*#__PURE__*/React.createElement("div", {
ref: thumbRef,
className: (0, _clsx.clsx)(`${scrollbarPrefixCls}-thumb`, {
[`${scrollbarPrefixCls}-thumb-moving`]: dragging
}),
style: {
...thumbStyle,
...propsThumbStyle
},
onMouseDown: onThumbMouseDown
}));
});
if (process.env.NODE_ENV !== 'production') {
ScrollBar.displayName = 'ScrollBar';
}
var _default = exports.default = ScrollBar;
@@ -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,27 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useChildren;
var React = _interopRequireWildcard(require("react"));
var _Item = require("../Item");
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.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,22 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useDiffItem;
var React = _interopRequireWildcard(require("react"));
var _algorithmUtil = require("../utils/algorithmUtil");
function useDiffItem(data, getKey, onDiff) {
const [prevData, setPrevData] = React.useState(data);
const [diffItem, setDiffItem] = React.useState(null);
React.useEffect(() => {
const diff = (0, _algorithmUtil.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,102 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useFrameWheel;
var _raf = _interopRequireDefault(require("@rc-component/util/lib/raf"));
var _react = require("react");
var _isFirefox = _interopRequireDefault(require("../utils/isFirefox"));
var _useOriginScroll = _interopRequireDefault(require("./useOriginScroll"));
function useFrameWheel(inVirtual, isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight, horizontalScroll,
/***
* Return `true` when you need to prevent default event
*/
onWheelDelta) {
const offsetRef = (0, _react.useRef)(0);
const nextFrameRef = (0, _react.useRef)(null);
// Firefox patch
const wheelValueRef = (0, _react.useRef)(null);
const isMouseScrollRef = (0, _react.useRef)(false);
// Scroll status sync
const originScroll = (0, _useOriginScroll.default)(isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight);
function onWheelY(e, deltaY) {
_raf.default.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 (!_isFirefox.default) {
event.preventDefault();
}
nextFrameRef.current = (0, _raf.default)(() => {
// 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 (!_isFirefox.default) {
event.preventDefault();
}
}
// Check for which direction does wheel do. `sx` means `shift + wheel`
const wheelDirectionRef = (0, _react.useRef)(null);
const wheelDirectionCleanRef = (0, _react.useRef)(null);
function onWheel(event) {
if (!inVirtual) return;
// Wait for 2 frame to clean direction
_raf.default.cancel(wheelDirectionCleanRef.current);
wheelDirectionCleanRef.current = (0, _raf.default)(() => {
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,46 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useGetSize = useGetSize;
var React = _interopRequireWildcard(require("react"));
/**
* Size info need loop query for the `heights` which will has the perf issue.
* Let cache result for each render phase.
*/
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,87 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useHeights;
var _react = _interopRequireWildcard(require("react"));
var React = _react;
var _CacheMap = _interopRequireDefault(require("../utils/CacheMap"));
function parseNumber(value) {
const num = parseFloat(value);
return isNaN(num) ? 0 : num;
}
function useHeights(getKey, onItemAdd, onItemRemove) {
const [updatedMark, setUpdatedMark] = React.useState(0);
const instanceRef = (0, _react.useRef)(new Map());
const heightsRef = (0, _react.useRef)(new _CacheMap.default());
const promiseIdRef = (0, _react.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);
}
}
}
(0, _react.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,93 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useMobileTouchMove;
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var _react = require("react");
const SMOOTH_PTG = 14 / 15;
function useMobileTouchMove(inVirtual, listRef, callback) {
const touchedRef = (0, _react.useRef)(false);
const touchXRef = (0, _react.useRef)(0);
const touchYRef = (0, _react.useRef)(0);
const elementRef = (0, _react.useRef)(null);
// Smooth scroll
const intervalRef = (0, _react.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);
}
};
(0, _useLayoutEffect.default)(() => {
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,50 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = require("react");
var _default = (isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight) => {
// Do lock for a wheel when scrolling
const lockRef = (0, _react.useRef)(false);
const lockTimeoutRef = (0, _react.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 = (0, _react.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;
};
};
exports.default = _default;
@@ -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,85 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useScrollDrag;
exports.getPageXY = getPageXY;
var _raf = _interopRequireDefault(require("@rc-component/util/lib/raf"));
var React = _interopRequireWildcard(require("react"));
function smoothScrollOffset(offset) {
return Math.floor(offset ** 0.5);
}
function getPageXY(e, horizontal) {
const obj = 'touches' in e ? e.touches[0] : e;
return obj[horizontal ? 'pageX' : 'pageY'] - window[horizontal ? 'scrollX' : 'scrollY'];
}
function useScrollDrag(inVirtual, componentRef, onScrollOffset) {
React.useEffect(() => {
const ele = componentRef.current;
if (inVirtual && ele) {
let mouseDownLock = false;
let rafId;
let offset;
const stopScroll = () => {
_raf.default.cancel(rafId);
};
const continueScroll = () => {
stopScroll();
rafId = (0, _raf.default)(() => {
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,154 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useScrollTo;
var React = _interopRequireWildcard(require("react"));
var _raf = _interopRequireDefault(require("@rc-component/util/lib/raf"));
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var _util = require("@rc-component/util");
/* eslint-disable no-param-reassign */
const MAX_TIMES = 10;
function useScrollTo(containerRef, data, heights, itemHeight, getKey, collectHeight, syncScrollTop, triggerFlash) {
const scrollRef = React.useRef();
const [syncState, setSyncState] = React.useState(null);
// ========================== Sync Scroll ==========================
(0, _useLayoutEffect.default)(() => {
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) {
(0, _util.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.default.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
});
}
};
}
@@ -0,0 +1,3 @@
import List from './List';
export type { ListRef, ListProps } from './List';
export default List;
+9
View File
@@ -0,0 +1,9 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _List = _interopRequireDefault(require("./List"));
var _default = exports.default = _List.default;
@@ -0,0 +1,26 @@
/// <reference types="react" />
export type RenderFunc<T> = (item: T, index: number, props: {
style: React.CSSProperties;
offsetX: number;
}) => React.ReactNode;
export interface SharedConfig<T> {
getKey: (item: T) => React.Key;
}
export type GetKey<T> = (item: T) => React.Key;
export type GetSize = (startKey: React.Key, endKey?: React.Key) => {
top: number;
bottom: number;
};
export interface ExtraRenderInfo {
/** Virtual list start line */
start: number;
/** Virtual list end line */
end: number;
/** Is current in virtual render */
virtual: boolean;
/** Used for `scrollWidth` tell the horizontal offset */
offsetX: number;
offsetY: number;
rtl: boolean;
getSize: GetSize;
}
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
@@ -0,0 +1,6 @@
import * as React from 'react';
import type { ListProps, ListRef } from './List';
declare const List: <Item = any>(props: React.PropsWithChildren<ListProps<Item>> & {
ref?: React.Ref<ListRef>;
}) => React.ReactElement;
export default List;
+15
View File
@@ -0,0 +1,15 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
var _List = require("./List");
const List = /*#__PURE__*/React.forwardRef((props, ref) => (0, _List.RawList)({
...props,
virtual: false
}, ref));
List.displayName = 'List';
var _default = exports.default = List;
@@ -0,0 +1,16 @@
import type React from 'react';
declare class CacheMap {
maps: Record<string, number>;
id: number;
diffRecords: Map<React.Key, number>;
constructor();
set(key: React.Key, value: number): void;
get(key: React.Key): number;
/**
* CacheMap will record the key changed.
* To help to know what's update in the next render.
*/
resetRecord(): void;
getRecord(): Map<React.Key, number>;
}
export default CacheMap;
@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
// Firefox has low performance of map.
class CacheMap {
maps;
// Used for cache key
// `useMemo` no need to update if `id` not change
id = 0;
diffRecords = new Map();
constructor() {
this.maps = Object.create(null);
}
set(key, value) {
// Record prev value
this.diffRecords.set(key, this.maps[key]);
this.maps[key] = value;
this.id += 1;
}
get(key) {
return this.maps[key];
}
/**
* CacheMap will record the key changed.
* To help to know what's update in the next render.
*/
resetRecord() {
this.diffRecords.clear();
}
getRecord() {
return this.diffRecords;
}
}
var _default = exports.default = CacheMap;
@@ -0,0 +1,23 @@
import type * as React from 'react';
/**
* Get index with specific start index one by one. e.g.
* min: 3, max: 9, start: 6
*
* Return index is:
* [0]: 6
* [1]: 7
* [2]: 5
* [3]: 8
* [4]: 4
* [5]: 9
* [6]: 3
*/
export declare function getIndexByStartLoc(min: number, max: number, start: number, index: number): number;
/**
* We assume that 2 list has only 1 item diff and others keeping the order.
* So we can use dichotomy algorithm to find changed one.
*/
export declare function findListDiffIndex<T>(originList: T[], targetList: T[], getKey: (item: T) => React.Key): {
index: number;
multiple: boolean;
} | null;
@@ -0,0 +1,87 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.findListDiffIndex = findListDiffIndex;
exports.getIndexByStartLoc = getIndexByStartLoc;
/**
* Get index with specific start index one by one. e.g.
* min: 3, max: 9, start: 6
*
* Return index is:
* [0]: 6
* [1]: 7
* [2]: 5
* [3]: 8
* [4]: 4
* [5]: 9
* [6]: 3
*/
function getIndexByStartLoc(min, max, start, index) {
const beforeCount = start - min;
const afterCount = max - start;
const balanceCount = Math.min(beforeCount, afterCount) * 2;
// Balance
if (index <= balanceCount) {
const stepIndex = Math.floor(index / 2);
if (index % 2) {
return start + stepIndex + 1;
}
return start - stepIndex;
}
// One is out of range
if (beforeCount > afterCount) {
return start - (index - afterCount);
}
return start + (index - beforeCount);
}
/**
* We assume that 2 list has only 1 item diff and others keeping the order.
* So we can use dichotomy algorithm to find changed one.
*/
function findListDiffIndex(originList, targetList, getKey) {
const originLen = originList.length;
const targetLen = targetList.length;
let shortList;
let longList;
if (originLen === 0 && targetLen === 0) {
return null;
}
if (originLen < targetLen) {
shortList = originList;
longList = targetList;
} else {
shortList = targetList;
longList = originList;
}
const notExistKey = {
__EMPTY_ITEM__: true
};
function getItemKey(item) {
if (item !== undefined) {
return getKey(item);
}
return notExistKey;
}
// Loop to find diff one
let diffIndex = null;
let multiple = Math.abs(originLen - targetLen) !== 1;
for (let i = 0; i < longList.length; i += 1) {
const shortKey = getItemKey(shortList[i]);
const longKey = getItemKey(longList[i]);
if (shortKey !== longKey) {
diffIndex = i;
multiple = multiple || shortKey !== getItemKey(longList[i + 1]);
break;
}
}
return diffIndex === null ? null : {
index: diffIndex,
multiple
};
}
@@ -0,0 +1,2 @@
declare const isFF: boolean;
export default isFF;
@@ -0,0 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
const isFF = typeof navigator === 'object' && /Firefox/i.test(navigator.userAgent);
var _default = exports.default = isFF;
@@ -0,0 +1 @@
export declare function getSpinSize(containerSize?: number, scrollRange?: number): number;
@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getSpinSize = getSpinSize;
const MIN_SIZE = 20;
function getSpinSize(containerSize = 0, scrollRange = 0) {
let baseSize = containerSize / scrollRange * containerSize;
if (isNaN(baseSize)) {
baseSize = 0;
}
baseSize = Math.max(baseSize, MIN_SIZE);
return Math.floor(baseSize);
}