@rc-component/tree
Version:
tree ui component for react
249 lines (243 loc) • 7.73 kB
JavaScript
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
/**
* Handle virtual list of the TreeNodes.
*/
import { getId, useId, useLayoutEffect } from '@rc-component/util';
import VirtualList from '@rc-component/virtual-list';
import * as React from 'react';
import MotionTreeNode from "./MotionTreeNode";
import { findExpandedKeys, getExpandRange } from "./utils/diffUtil";
import { getKey, getTreeNodeProps } from "./utils/treeUtil";
export const MOTION_KEY = `RC_TREE_MOTION_${Math.random()}`;
const MotionNode = {
key: MOTION_KEY
};
export const MotionEntity = {
key: MOTION_KEY,
level: 0,
index: 0,
pos: '0',
node: MotionNode,
nodes: [MotionNode]
};
const MotionFlattenData = {
parent: null,
children: [],
pos: MotionEntity.pos,
data: MotionNode,
title: null,
key: MOTION_KEY,
/** Hold empty list here since we do not use it */
isStart: [],
isEnd: []
};
/**
* We only need get visible content items to play the animation.
*/
export function getMinimumRangeTransitionRange(list, virtual, height, itemHeight) {
if (virtual === false || !height) {
return list;
}
return list.slice(0, Math.ceil(height / itemHeight) + 1);
}
function itemKey(item) {
const {
key,
pos
} = item;
return getKey(key, pos);
}
const NodeList = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls,
data,
selectable,
checkable,
expandedKeys,
selectedKeys,
checkedKeys,
loadedKeys,
loadingKeys,
halfCheckedKeys,
keyEntities,
disabled,
dragging,
dragOverNodeKey,
dropPosition,
motion,
height,
itemHeight,
virtual,
scrollWidth,
focusable,
activeItem,
tabIndex,
onKeyDown,
onFocus,
onBlur,
onMouseDown,
onActiveChange,
onListChangeStart,
onListChangeEnd,
...domProps
} = props;
const treeId = useId();
// =============================== Ref ================================
const listRef = React.useRef(null);
const indentMeasurerRef = React.useRef(null);
React.useImperativeHandle(ref, () => ({
scrollTo: scroll => {
listRef.current.scrollTo(scroll);
},
getIndentWidth: () => indentMeasurerRef.current.offsetWidth
}));
// ============================== Motion ==============================
const [prevExpandedKeys, setPrevExpandedKeys] = React.useState(expandedKeys);
const [prevData, setPrevData] = React.useState(data);
const [transitionData, setTransitionData] = React.useState(data);
const [transitionRange, setTransitionRange] = React.useState([]);
const [motionType, setMotionType] = React.useState(null);
// When motion end but data change, this will makes data back to previous one
const dataRef = React.useRef(data);
dataRef.current = data;
function onMotionEnd() {
const latestData = dataRef.current;
setPrevData(latestData);
setTransitionData(latestData);
setTransitionRange([]);
setMotionType(null);
onListChangeEnd();
}
// Do animation if expanded keys changed
// layoutEffect here to avoid blink of node removing
useLayoutEffect(() => {
setPrevExpandedKeys(expandedKeys);
const diffExpanded = findExpandedKeys(prevExpandedKeys, expandedKeys);
if (diffExpanded.key !== null) {
if (diffExpanded.add) {
const keyIndex = prevData.findIndex(({
key
}) => key === diffExpanded.key);
const rangeNodes = getMinimumRangeTransitionRange(getExpandRange(prevData, data, diffExpanded.key), virtual, height, itemHeight);
const newTransitionData = prevData.slice();
newTransitionData.splice(keyIndex + 1, 0, MotionFlattenData);
setTransitionData(newTransitionData);
setTransitionRange(rangeNodes);
setMotionType('show');
} else {
const keyIndex = data.findIndex(({
key
}) => key === diffExpanded.key);
const rangeNodes = getMinimumRangeTransitionRange(getExpandRange(data, prevData, diffExpanded.key), virtual, height, itemHeight);
const newTransitionData = data.slice();
newTransitionData.splice(keyIndex + 1, 0, MotionFlattenData);
setTransitionData(newTransitionData);
setTransitionRange(rangeNodes);
setMotionType('hide');
}
} else if (prevData !== data) {
// If whole data changed, we just refresh the list
setPrevData(data);
setTransitionData(data);
}
}, [expandedKeys, data]);
// We should clean up motion if is changed by dragging
React.useEffect(() => {
if (!dragging) {
onMotionEnd();
}
}, [dragging]);
const mergedData = motion ? transitionData : data;
const treeNodeRequiredProps = {
expandedKeys,
selectedKeys,
loadedKeys,
loadingKeys,
checkedKeys,
halfCheckedKeys,
dragOverNodeKey,
dropPosition,
keyEntities
};
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-treenode`,
"aria-hidden": true,
style: {
position: 'absolute',
pointerEvents: 'none',
visibility: 'hidden',
height: 0,
overflow: 'hidden',
border: 0,
padding: 0
}
}, /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-indent`
}, /*#__PURE__*/React.createElement("div", {
ref: indentMeasurerRef,
className: `${prefixCls}-indent-unit`
}))), /*#__PURE__*/React.createElement(VirtualList, _extends({}, domProps, {
data: mergedData,
itemKey: itemKey,
height: height,
fullHeight: false,
virtual: virtual,
itemHeight: itemHeight,
scrollWidth: scrollWidth,
prefixCls: `${prefixCls}-list`,
ref: listRef,
role: "tree",
tabIndex: focusable !== false && !disabled ? tabIndex : undefined,
"aria-activedescendant": activeItem ? getId(treeId, activeItem.key) : undefined,
onKeyDown: onKeyDown,
onFocus: onFocus,
onBlur: onBlur,
onMouseDown: onMouseDown,
onVisibleChange: originList => {
// The best match is using `fullList` - `originList` = `restList`
// and check the `restList` to see if has the MOTION_KEY node
// but this will cause performance issue for long list compare
// we just check `originList` and repeat trigger `onMotionEnd`
if (originList.every(item => itemKey(item) !== MOTION_KEY)) {
onMotionEnd();
}
}
}), treeNode => {
const {
pos,
data: {
...restProps
},
title,
key,
isStart,
isEnd
} = treeNode;
const mergedKey = getKey(key, pos);
delete restProps.key;
delete restProps.children;
const treeNodeProps = getTreeNodeProps(mergedKey, treeNodeRequiredProps);
return /*#__PURE__*/React.createElement(MotionTreeNode, _extends({}, restProps, treeNodeProps, {
title: title,
active: !!activeItem && key === activeItem.key,
pos: pos,
data: treeNode.data,
isStart: isStart,
isEnd: isEnd,
motion: motion,
motionNodes: key === MOTION_KEY ? transitionRange : null,
motionType: motionType,
onMotionStart: onListChangeStart,
onMotionEnd: onMotionEnd,
treeNodeRequiredProps: treeNodeRequiredProps,
treeId: treeId,
onMouseMove: () => {
onActiveChange(null);
}
}));
}));
});
if (process.env.NODE_ENV !== 'production') {
NodeList.displayName = 'NodeList';
}
export default NodeList;