1
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
import * as React from 'react';
|
||||
export declare const WheelLockContext: React.Context<(lock: boolean) => void>;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import * as React from 'react';
|
||||
export const WheelLockContext = /*#__PURE__*/React.createContext(() => {});
|
||||
+21
@@ -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;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import _extends from "@babel/runtime/helpers/esm/extends";
|
||||
import * as React from 'react';
|
||||
import ResizeObserver from '@rc-component/resize-observer';
|
||||
import { clsx } from '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, {
|
||||
onResize: ({
|
||||
offsetHeight
|
||||
}) => {
|
||||
if (offsetHeight && onInnerResize) {
|
||||
onInnerResize();
|
||||
}
|
||||
}
|
||||
}, /*#__PURE__*/React.createElement("div", _extends({
|
||||
style: innerStyle,
|
||||
className: clsx({
|
||||
[`${prefixCls}-holder-inner`]: prefixCls
|
||||
}),
|
||||
ref: ref
|
||||
}, innerProps), children, extra)));
|
||||
});
|
||||
Filler.displayName = 'Filler';
|
||||
export default Filler;
|
||||
+6
@@ -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>>;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
export function Item({
|
||||
children,
|
||||
setRef
|
||||
}) {
|
||||
const refFunc = React.useCallback(node => {
|
||||
setRef(node);
|
||||
}, []);
|
||||
return /*#__PURE__*/React.cloneElement(children, {
|
||||
ref: refFunc
|
||||
});
|
||||
}
|
||||
+61
@@ -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;
|
||||
+512
@@ -0,0 +1,512 @@
|
||||
import _extends from "@babel/runtime/helpers/esm/extends";
|
||||
import { clsx } from 'clsx';
|
||||
import ResizeObserver from '@rc-component/resize-observer';
|
||||
import { useEvent } from '@rc-component/util';
|
||||
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
|
||||
import * as React from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import Filler from "./Filler";
|
||||
import useChildren from "./hooks/useChildren";
|
||||
import useDiffItem from "./hooks/useDiffItem";
|
||||
import useFrameWheel from "./hooks/useFrameWheel";
|
||||
import { useGetSize } from "./hooks/useGetSize";
|
||||
import useHeights from "./hooks/useHeights";
|
||||
import useMobileTouchMove from "./hooks/useMobileTouchMove";
|
||||
import useOriginScroll from "./hooks/useOriginScroll";
|
||||
import useScrollDrag from "./hooks/useScrollDrag";
|
||||
import useScrollTo from "./hooks/useScrollTo";
|
||||
import ScrollBar from "./ScrollBar";
|
||||
import { getSpinSize } from "./utils/scrollbarUtil";
|
||||
const EMPTY_DATA = [];
|
||||
const ScrollStyle = {
|
||||
overflowY: 'auto',
|
||||
overflowAnchor: 'none'
|
||||
};
|
||||
export 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] = useHeights(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 = clsx(prefixCls, {
|
||||
[`${prefixCls}-rtl`]: isRTL
|
||||
}, className);
|
||||
const mergedData = data || EMPTY_DATA;
|
||||
const componentRef = useRef();
|
||||
const fillerInnerRef = useRef();
|
||||
const containerRef = useRef();
|
||||
|
||||
// =============================== Item Key ===============================
|
||||
|
||||
const [offsetTop, setOffsetTop] = useState(0);
|
||||
const [offsetLeft, setOffsetLeft] = useState(0);
|
||||
const [scrollMoving, setScrollMoving] = 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 = useRef({
|
||||
start: 0,
|
||||
end: mergedData.length
|
||||
});
|
||||
const diffItemRef = useRef();
|
||||
const [diffItem] = useDiffItem(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 = useRef();
|
||||
const horizontalScrollBarRef = useRef();
|
||||
const horizontalScrollBarSpinSize = React.useMemo(() => getSpinSize(size.width, scrollWidth), [size.width, scrollWidth]);
|
||||
const verticalScrollBarSpinSize = React.useMemo(() => getSpinSize(size.height, scrollHeight), [size.height, scrollHeight]);
|
||||
|
||||
// =============================== In Range ===============================
|
||||
const maxScrollHeight = scrollHeight - height;
|
||||
const maxScrollHeightRef = 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 = useOriginScroll(isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight);
|
||||
|
||||
// ================================ Scroll ================================
|
||||
const getVirtualScrollInfo = () => ({
|
||||
x: isRTL ? -offsetLeft : offsetLeft,
|
||||
y: offsetTop
|
||||
});
|
||||
const lastVirtualScrollInfoRef = useRef(getVirtualScrollInfo());
|
||||
const triggerScroll = 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) {
|
||||
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 = useEvent((offsetXY, fromHorizontal) => {
|
||||
if (fromHorizontal) {
|
||||
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] = useFrameWheel(useVirtual, isScrollAtTop, isScrollAtBottom, isScrollAtLeft, isScrollAtRight, !!scrollWidth, onWheelDelta);
|
||||
|
||||
// Mobile touch move
|
||||
useMobileTouchMove(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
|
||||
useScrollDrag(inVirtual, componentRef, offset => {
|
||||
syncScrollTop(top => top + offset);
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
// 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
|
||||
useLayoutEffect(() => {
|
||||
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 = useScrollTo(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 */
|
||||
useLayoutEffect(() => {
|
||||
if (onVisibleChange) {
|
||||
const renderList = mergedData.slice(start, end + 1);
|
||||
onVisibleChange(renderList, mergedData);
|
||||
}
|
||||
}, [start, end, mergedData]);
|
||||
|
||||
// ================================ Extra =================================
|
||||
const getSize = useGetSize(mergedData, getKey, heights, itemHeight);
|
||||
const extraContent = extraRender?.({
|
||||
start,
|
||||
end,
|
||||
virtual: inVirtual,
|
||||
offsetX: offsetLeft,
|
||||
offsetY: fillerOffset,
|
||||
rtl: isRTL,
|
||||
getSize
|
||||
});
|
||||
|
||||
// ================================ Render ================================
|
||||
const listChildren = useChildren(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", _extends({
|
||||
ref: containerRef,
|
||||
style: {
|
||||
...style,
|
||||
position: 'relative'
|
||||
},
|
||||
className: mergedClassName
|
||||
}, containerProps, restProps), /*#__PURE__*/React.createElement(ResizeObserver, {
|
||||
onResize: onHolderResize
|
||||
}, /*#__PURE__*/React.createElement(Component, {
|
||||
className: `${prefixCls}-holder`,
|
||||
style: componentStyle,
|
||||
ref: componentRef,
|
||||
onScroll: onFallbackScroll,
|
||||
onMouseEnter: delayHideScrollBar
|
||||
}, /*#__PURE__*/React.createElement(Filler, {
|
||||
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, {
|
||||
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, {
|
||||
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';
|
||||
export default List;
|
||||
+22
@@ -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;
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
import { clsx } from 'clsx';
|
||||
import raf from "@rc-component/util/es/raf";
|
||||
import * as React from 'react';
|
||||
import { getPageXY } from "./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(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.cancel(moveRafId);
|
||||
const rect = scrollbarRef.current.getBoundingClientRect();
|
||||
const scale = containerSize / (horizontal ? rect.width : rect.height);
|
||||
if (stateDragging) {
|
||||
const offset = (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 = raf(() => {
|
||||
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.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: 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: clsx(`${scrollbarPrefixCls}-thumb`, {
|
||||
[`${scrollbarPrefixCls}-thumb-moving`]: dragging
|
||||
}),
|
||||
style: {
|
||||
...thumbStyle,
|
||||
...propsThumbStyle
|
||||
},
|
||||
onMouseDown: onThumbMouseDown
|
||||
}));
|
||||
});
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
ScrollBar.displayName = 'ScrollBar';
|
||||
}
|
||||
export default ScrollBar;
|
||||
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
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import List from './List';
|
||||
export type { ListRef, ListProps } from './List';
|
||||
export default List;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import List from "./List";
|
||||
export default List;
|
||||
+26
@@ -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;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+6
@@ -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;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { RawList } from "./List";
|
||||
const List = /*#__PURE__*/React.forwardRef((props, ref) => RawList({
|
||||
...props,
|
||||
virtual: false
|
||||
}, ref));
|
||||
List.displayName = 'List';
|
||||
export default List;
|
||||
+16
@@ -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;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
export default CacheMap;
|
||||
Generated
Vendored
+23
@@ -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;
|
||||
Generated
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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 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.
|
||||
*/
|
||||
export 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
|
||||
};
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
declare const isFF: boolean;
|
||||
export default isFF;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
const isFF = typeof navigator === 'object' && /Firefox/i.test(navigator.userAgent);
|
||||
export default isFF;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function getSpinSize(containerSize?: number, scrollRange?: number): number;
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
const MIN_SIZE = 20;
|
||||
export 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);
|
||||
}
|
||||
Reference in New Issue
Block a user