UNPKG

@rc-component/tree

Version:
1,242 lines (1,200 loc) 36.2 kB
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); } // TODO: https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/examples/treeview/treeview-2/treeview-2a.html // Fully accessibility support import classNames from 'classnames'; import KeyCode from "@rc-component/util/es/KeyCode"; import pickAttrs from "@rc-component/util/es/pickAttrs"; import warning from "@rc-component/util/es/warning"; import * as React from 'react'; import { TreeContext } from "./contextTypes"; import DropIndicator from "./DropIndicator"; import NodeList, { MOTION_KEY, MotionEntity } from "./NodeList"; import TreeNode from "./TreeNode"; import { arrAdd, arrDel, calcDropPosition, calcSelectedKeys, conductExpandParent, getDragChildrenKeys, parseCheckedKeys, posToArr } from "./util"; import { conductCheck } from "./utils/conductUtil"; import getEntity from "./utils/keyUtil"; import { convertDataToEntities, convertNodePropsToEventData, convertTreeToData, fillFieldNames, flattenTreeData, getTreeNodeProps, warningWithoutKey } from "./utils/treeUtil"; const MAX_RETRY_TIMES = 10; class Tree extends React.Component { static defaultProps = { prefixCls: 'rc-tree', showLine: false, showIcon: true, selectable: true, multiple: false, checkable: false, disabled: false, checkStrictly: false, draggable: false, defaultExpandParent: true, autoExpandParent: false, defaultExpandAll: false, defaultExpandedKeys: [], defaultCheckedKeys: [], defaultSelectedKeys: [], dropIndicatorRender: DropIndicator, allowDrop: () => true, expandAction: false }; static TreeNode = TreeNode; destroyed = false; delayedDragEnterLogic; loadingRetryTimes = {}; state = { keyEntities: {}, indent: null, selectedKeys: [], checkedKeys: [], halfCheckedKeys: [], loadedKeys: [], loadingKeys: [], expandedKeys: [], draggingNodeKey: null, dragChildrenKeys: [], // dropTargetKey is the key of abstract-drop-node // the abstract-drop-node is the real drop node when drag and drop // not the DOM drag over node dropTargetKey: null, dropPosition: null, // the drop position of abstract-drop-node, inside 0, top -1, bottom 1 dropContainerKey: null, // the container key of abstract-drop-node if dropPosition is -1 or 1 dropLevelOffset: null, // the drop level offset of abstract-drag-over-node dropTargetPos: null, // the pos of abstract-drop-node dropAllowed: true, // if drop to abstract-drop-node is allowed // the abstract-drag-over-node // if mouse is on the bottom of top dom node or no the top of the bottom dom node // abstract-drag-over-node is the top node dragOverNodeKey: null, treeData: [], flattenNodes: [], focused: false, activeKey: null, listChanging: false, prevProps: null, fieldNames: fillFieldNames() }; dragStartMousePosition = null; dragNodeProps = null; currentMouseOverDroppableNodeKey = null; listRef = /*#__PURE__*/React.createRef(); componentDidMount() { this.destroyed = false; this.onUpdated(); } componentDidUpdate() { this.onUpdated(); } onUpdated() { const { activeKey, itemScrollOffset = 0 } = this.props; if (activeKey !== undefined && activeKey !== this.state.activeKey) { this.setState({ activeKey }); if (activeKey !== null) { this.scrollTo({ key: activeKey, offset: itemScrollOffset }); } } } componentWillUnmount() { window.removeEventListener('dragend', this.onWindowDragEnd); this.destroyed = true; } static getDerivedStateFromProps(props, prevState) { const { prevProps } = prevState; const newState = { prevProps: props }; function needSync(name) { return !prevProps && props.hasOwnProperty(name) || prevProps && prevProps[name] !== props[name]; } // ================== Tree Node ================== let treeData; // fieldNames let { fieldNames } = prevState; if (needSync('fieldNames')) { fieldNames = fillFieldNames(props.fieldNames); newState.fieldNames = fieldNames; } // Check if `treeData` or `children` changed and save into the state. if (needSync('treeData')) { ({ treeData } = props); } else if (needSync('children')) { warning(false, '`children` of Tree is deprecated. Please use `treeData` instead.'); treeData = convertTreeToData(props.children); } // Save flatten nodes info and convert `treeData` into keyEntities if (treeData) { newState.treeData = treeData; const entitiesMap = convertDataToEntities(treeData, { fieldNames }); newState.keyEntities = { [MOTION_KEY]: MotionEntity, ...entitiesMap.keyEntities }; // Warning if treeNode not provide key if (process.env.NODE_ENV !== 'production') { warningWithoutKey(treeData, fieldNames); } } const keyEntities = newState.keyEntities || prevState.keyEntities; // ================ expandedKeys ================= if (needSync('expandedKeys') || prevProps && needSync('autoExpandParent')) { newState.expandedKeys = props.autoExpandParent || !prevProps && props.defaultExpandParent ? conductExpandParent(props.expandedKeys, keyEntities) : props.expandedKeys; } else if (!prevProps && props.defaultExpandAll) { const cloneKeyEntities = { ...keyEntities }; delete cloneKeyEntities[MOTION_KEY]; // Only take the key who has the children to enhance the performance const nextExpandedKeys = []; Object.keys(cloneKeyEntities).forEach(key => { const entity = cloneKeyEntities[key]; if (entity.children && entity.children.length) { nextExpandedKeys.push(entity.key); } }); newState.expandedKeys = nextExpandedKeys; } else if (!prevProps && props.defaultExpandedKeys) { newState.expandedKeys = props.autoExpandParent || props.defaultExpandParent ? conductExpandParent(props.defaultExpandedKeys, keyEntities) : props.defaultExpandedKeys; } if (!newState.expandedKeys) { delete newState.expandedKeys; } // ================ flattenNodes ================= if (treeData || newState.expandedKeys) { const flattenNodes = flattenTreeData(treeData || prevState.treeData, newState.expandedKeys || prevState.expandedKeys, fieldNames); newState.flattenNodes = flattenNodes; } // ================ selectedKeys ================= if (props.selectable) { if (needSync('selectedKeys')) { newState.selectedKeys = calcSelectedKeys(props.selectedKeys, props); } else if (!prevProps && props.defaultSelectedKeys) { newState.selectedKeys = calcSelectedKeys(props.defaultSelectedKeys, props); } } // ================= checkedKeys ================= if (props.checkable) { let checkedKeyEntity; if (needSync('checkedKeys')) { checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {}; } else if (!prevProps && props.defaultCheckedKeys) { checkedKeyEntity = parseCheckedKeys(props.defaultCheckedKeys) || {}; } else if (treeData) { // If `treeData` changed, we also need check it checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || { checkedKeys: prevState.checkedKeys, halfCheckedKeys: prevState.halfCheckedKeys }; } if (checkedKeyEntity) { let { checkedKeys = [], halfCheckedKeys = [] } = checkedKeyEntity; if (!props.checkStrictly) { const conductKeys = conductCheck(checkedKeys, true, keyEntities); ({ checkedKeys, halfCheckedKeys } = conductKeys); } newState.checkedKeys = checkedKeys; newState.halfCheckedKeys = halfCheckedKeys; } } // ================= loadedKeys ================== if (needSync('loadedKeys')) { newState.loadedKeys = props.loadedKeys; } return newState; } onNodeDragStart = (event, nodeProps) => { const { expandedKeys, keyEntities } = this.state; const { onDragStart } = this.props; const { eventKey } = nodeProps; this.dragNodeProps = nodeProps; this.dragStartMousePosition = { x: event.clientX, y: event.clientY }; const newExpandedKeys = arrDel(expandedKeys, eventKey); this.setState({ draggingNodeKey: eventKey, dragChildrenKeys: getDragChildrenKeys(eventKey, keyEntities), indent: this.listRef.current.getIndentWidth() }); this.setExpandedKeys(newExpandedKeys); window.addEventListener('dragend', this.onWindowDragEnd); onDragStart?.({ event, node: convertNodePropsToEventData(nodeProps) }); }; /** * [Legacy] Select handler is smaller than node, * so that this will trigger when drag enter node or select handler. * This is a little tricky if customize css without padding. * Better for use mouse move event to refresh drag state. * But let's just keep it to avoid event trigger logic change. */ onNodeDragEnter = (event, nodeProps) => { const { expandedKeys, keyEntities, dragChildrenKeys, flattenNodes, indent } = this.state; const { onDragEnter, onExpand, allowDrop, direction } = this.props; const { pos, eventKey } = nodeProps; // record the key of node which is latest entered, used in dragleave event. if (this.currentMouseOverDroppableNodeKey !== eventKey) { this.currentMouseOverDroppableNodeKey = eventKey; } if (!this.dragNodeProps) { this.resetDragState(); return; } const { dropPosition, dropLevelOffset, dropTargetKey, dropContainerKey, dropTargetPos, dropAllowed, dragOverNodeKey } = calcDropPosition(event, this.dragNodeProps, nodeProps, indent, this.dragStartMousePosition, allowDrop, flattenNodes, keyEntities, expandedKeys, direction); if ( // don't allow drop inside its children dragChildrenKeys.includes(dropTargetKey) || // don't allow drop when drop is not allowed caculated by calcDropPosition !dropAllowed) { this.resetDragState(); return; } // Side effect for delay drag if (!this.delayedDragEnterLogic) { this.delayedDragEnterLogic = {}; } Object.keys(this.delayedDragEnterLogic).forEach(key => { clearTimeout(this.delayedDragEnterLogic[key]); }); if (this.dragNodeProps.eventKey !== nodeProps.eventKey) { // hoist expand logic here // since if logic is on the bottom // it will be blocked by abstract dragover node check // => if you dragenter from top, you mouse will still be consider as in the top node event.persist(); this.delayedDragEnterLogic[pos] = window.setTimeout(() => { if (this.state.draggingNodeKey === null) { return; } let newExpandedKeys = [...expandedKeys]; const entity = getEntity(keyEntities, nodeProps.eventKey); if (entity && (entity.children || []).length) { newExpandedKeys = arrAdd(expandedKeys, nodeProps.eventKey); } if (!this.props.hasOwnProperty('expandedKeys')) { this.setExpandedKeys(newExpandedKeys); } onExpand?.(newExpandedKeys, { node: convertNodePropsToEventData(nodeProps), expanded: true, nativeEvent: event.nativeEvent }); }, 800); } // Skip if drag node is self if (this.dragNodeProps.eventKey === dropTargetKey && dropLevelOffset === 0) { this.resetDragState(); return; } // Update drag over node and drag state this.setState({ dragOverNodeKey, dropPosition, dropLevelOffset, dropTargetKey, dropContainerKey, dropTargetPos, dropAllowed }); onDragEnter?.({ event, node: convertNodePropsToEventData(nodeProps), expandedKeys }); }; onNodeDragOver = (event, nodeProps) => { const { dragChildrenKeys, flattenNodes, keyEntities, expandedKeys, indent } = this.state; const { onDragOver, allowDrop, direction } = this.props; if (!this.dragNodeProps) { return; } const { dropPosition, dropLevelOffset, dropTargetKey, dropContainerKey, dropTargetPos, dropAllowed, dragOverNodeKey } = calcDropPosition(event, this.dragNodeProps, nodeProps, indent, this.dragStartMousePosition, allowDrop, flattenNodes, keyEntities, expandedKeys, direction); if (dragChildrenKeys.includes(dropTargetKey) || !dropAllowed) { // don't allow drop inside its children // don't allow drop when drop is not allowed calculated by calcDropPosition return; } // Update drag position if (this.dragNodeProps.eventKey === dropTargetKey && dropLevelOffset === 0) { if (!(this.state.dropPosition === null && this.state.dropLevelOffset === null && this.state.dropTargetKey === null && this.state.dropContainerKey === null && this.state.dropTargetPos === null && this.state.dropAllowed === false && this.state.dragOverNodeKey === null)) { this.resetDragState(); } } else if (!(dropPosition === this.state.dropPosition && dropLevelOffset === this.state.dropLevelOffset && dropTargetKey === this.state.dropTargetKey && dropContainerKey === this.state.dropContainerKey && dropTargetPos === this.state.dropTargetPos && dropAllowed === this.state.dropAllowed && dragOverNodeKey === this.state.dragOverNodeKey)) { this.setState({ dropPosition, dropLevelOffset, dropTargetKey, dropContainerKey, dropTargetPos, dropAllowed, dragOverNodeKey }); } onDragOver?.({ event, node: convertNodePropsToEventData(nodeProps) }); }; onNodeDragLeave = (event, nodeProps) => { // if it is outside the droppable area // currentMouseOverDroppableNodeKey will be updated in dragenter event when into another droppable receiver. if (this.currentMouseOverDroppableNodeKey === nodeProps.eventKey && !event.currentTarget.contains(event.relatedTarget)) { this.resetDragState(); this.currentMouseOverDroppableNodeKey = null; } const { onDragLeave } = this.props; onDragLeave?.({ event, node: convertNodePropsToEventData(nodeProps) }); }; // since stopPropagation() is called in treeNode // if onWindowDrag is called, whice means state is keeped, drag state should be cleared onWindowDragEnd = event => { this.onNodeDragEnd(event, null, true); window.removeEventListener('dragend', this.onWindowDragEnd); }; // if onNodeDragEnd is called, onWindowDragEnd won't be called since stopPropagation() is called onNodeDragEnd = (event, nodeProps) => { const { onDragEnd } = this.props; this.setState({ dragOverNodeKey: null }); this.cleanDragState(); onDragEnd?.({ event, node: convertNodePropsToEventData(nodeProps) }); this.dragNodeProps = null; window.removeEventListener('dragend', this.onWindowDragEnd); }; onNodeDrop = (event, _, outsideTree = false) => { const { dragChildrenKeys, dropPosition, dropTargetKey, dropTargetPos, dropAllowed } = this.state; if (!dropAllowed) { return; } const { onDrop } = this.props; this.setState({ dragOverNodeKey: null }); this.cleanDragState(); if (dropTargetKey === null) return; const abstractDropNodeProps = { ...getTreeNodeProps(dropTargetKey, this.getTreeNodeRequiredProps()), active: this.getActiveItem()?.key === dropTargetKey, data: getEntity(this.state.keyEntities, dropTargetKey).node }; const dropToChild = dragChildrenKeys.includes(dropTargetKey); warning(!dropToChild, "Can not drop to dragNode's children node. This is a bug of rc-tree. Please report an issue."); const posArr = posToArr(dropTargetPos); const dropResult = { event, node: convertNodePropsToEventData(abstractDropNodeProps), dragNode: this.dragNodeProps ? convertNodePropsToEventData(this.dragNodeProps) : null, dragNodesKeys: [this.dragNodeProps.eventKey].concat(dragChildrenKeys), dropToGap: dropPosition !== 0, dropPosition: dropPosition + Number(posArr[posArr.length - 1]) }; if (!outsideTree) { onDrop?.(dropResult); } this.dragNodeProps = null; }; resetDragState() { this.setState({ dragOverNodeKey: null, dropPosition: null, dropLevelOffset: null, dropTargetKey: null, dropContainerKey: null, dropTargetPos: null, dropAllowed: false }); } cleanDragState = () => { const { draggingNodeKey } = this.state; if (draggingNodeKey !== null) { this.setState({ draggingNodeKey: null, dropPosition: null, dropContainerKey: null, dropTargetKey: null, dropLevelOffset: null, dropAllowed: true, dragOverNodeKey: null }); } this.dragStartMousePosition = null; this.currentMouseOverDroppableNodeKey = null; }; triggerExpandActionExpand = (e, treeNode) => { const { expandedKeys, flattenNodes } = this.state; const { expanded, key, isLeaf } = treeNode; if (isLeaf || e.shiftKey || e.metaKey || e.ctrlKey) { return; } const node = flattenNodes.filter(nodeItem => nodeItem.key === key)[0]; const eventNode = convertNodePropsToEventData({ ...getTreeNodeProps(key, this.getTreeNodeRequiredProps()), data: node.data }); this.setExpandedKeys(expanded ? arrDel(expandedKeys, key) : arrAdd(expandedKeys, key)); this.onNodeExpand(e, eventNode); }; onNodeClick = (e, treeNode) => { const { onClick, expandAction } = this.props; if (expandAction === 'click') { this.triggerExpandActionExpand(e, treeNode); } onClick?.(e, treeNode); }; onNodeDoubleClick = (e, treeNode) => { const { onDoubleClick, expandAction } = this.props; if (expandAction === 'doubleClick') { this.triggerExpandActionExpand(e, treeNode); } onDoubleClick?.(e, treeNode); }; onNodeSelect = (e, treeNode) => { let { selectedKeys } = this.state; const { keyEntities, fieldNames } = this.state; const { onSelect, multiple } = this.props; const { selected } = treeNode; const key = treeNode[fieldNames.key]; const targetSelected = !selected; // Update selected keys if (!targetSelected) { selectedKeys = arrDel(selectedKeys, key); } else if (!multiple) { selectedKeys = [key]; } else { selectedKeys = arrAdd(selectedKeys, key); } // [Legacy] Not found related usage in doc or upper libs const selectedNodes = selectedKeys.map(selectedKey => { const entity = getEntity(keyEntities, selectedKey); return entity ? entity.node : null; }).filter(Boolean); this.setUncontrolledState({ selectedKeys }); onSelect?.(selectedKeys, { event: 'select', selected: targetSelected, node: treeNode, selectedNodes, nativeEvent: e.nativeEvent }); }; onNodeCheck = (e, treeNode, checked) => { const { keyEntities, checkedKeys: oriCheckedKeys, halfCheckedKeys: oriHalfCheckedKeys } = this.state; const { checkStrictly, onCheck } = this.props; const { key } = treeNode; // Prepare trigger arguments let checkedObj; const eventObj = { event: 'check', node: treeNode, checked, nativeEvent: e.nativeEvent }; if (checkStrictly) { const checkedKeys = checked ? arrAdd(oriCheckedKeys, key) : arrDel(oriCheckedKeys, key); const halfCheckedKeys = arrDel(oriHalfCheckedKeys, key); checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys }; eventObj.checkedNodes = checkedKeys.map(checkedKey => getEntity(keyEntities, checkedKey)).filter(Boolean).map(entity => entity.node); this.setUncontrolledState({ checkedKeys }); } else { // Always fill first let { checkedKeys, halfCheckedKeys } = conductCheck([...oriCheckedKeys, key], true, keyEntities); // If remove, we do it again to correction if (!checked) { const keySet = new Set(checkedKeys); keySet.delete(key); ({ checkedKeys, halfCheckedKeys } = conductCheck(Array.from(keySet), { checked: false, halfCheckedKeys }, keyEntities)); } checkedObj = checkedKeys; // [Legacy] This is used for `rc-tree-select` eventObj.checkedNodes = []; eventObj.checkedNodesPositions = []; eventObj.halfCheckedKeys = halfCheckedKeys; checkedKeys.forEach(checkedKey => { const entity = getEntity(keyEntities, checkedKey); if (!entity) return; const { node, pos } = entity; eventObj.checkedNodes.push(node); eventObj.checkedNodesPositions.push({ node, pos }); }); this.setUncontrolledState({ checkedKeys }, false, { halfCheckedKeys }); } onCheck?.(checkedObj, eventObj); }; onNodeLoad = treeNode => { const { key } = treeNode; const { keyEntities } = this.state; // Skip if has children already const entity = getEntity(keyEntities, key); if (entity?.children?.length) { return; } const loadPromise = new Promise((resolve, reject) => { // We need to get the latest state of loading/loaded keys this.setState(({ loadedKeys = [], loadingKeys = [] }) => { const { loadData, onLoad } = this.props; if (!loadData || loadedKeys.includes(key) || loadingKeys.includes(key)) { return null; } // Process load data const promise = loadData(treeNode); promise.then(() => { const { loadedKeys: currentLoadedKeys } = this.state; const newLoadedKeys = arrAdd(currentLoadedKeys, key); // onLoad should trigger before internal setState to avoid `loadData` trigger twice. // https://github.com/ant-design/ant-design/issues/12464 onLoad?.(newLoadedKeys, { event: 'load', node: treeNode }); this.setUncontrolledState({ loadedKeys: newLoadedKeys }); this.setState(prevState => ({ loadingKeys: arrDel(prevState.loadingKeys, key) })); resolve(); }).catch(e => { this.setState(prevState => ({ loadingKeys: arrDel(prevState.loadingKeys, key) })); // If exceed max retry times, we give up retry this.loadingRetryTimes[key] = (this.loadingRetryTimes[key] || 0) + 1; if (this.loadingRetryTimes[key] >= MAX_RETRY_TIMES) { const { loadedKeys: currentLoadedKeys } = this.state; warning(false, 'Retry for `loadData` many times but still failed. No more retry.'); this.setUncontrolledState({ loadedKeys: arrAdd(currentLoadedKeys, key) }); resolve(); } reject(e); }); return { loadingKeys: arrAdd(loadingKeys, key) }; }); }); // Not care warning if we ignore this loadPromise.catch(() => {}); return loadPromise; }; onNodeMouseEnter = (event, node) => { const { onMouseEnter } = this.props; onMouseEnter?.({ event, node }); }; onNodeMouseLeave = (event, node) => { const { onMouseLeave } = this.props; onMouseLeave?.({ event, node }); }; onNodeContextMenu = (event, node) => { const { onRightClick } = this.props; if (onRightClick) { event.preventDefault(); onRightClick({ event, node }); } }; onFocus = (...args) => { const { onFocus } = this.props; this.setState({ focused: true }); onFocus?.(...args); }; onBlur = (...args) => { const { onBlur } = this.props; this.setState({ focused: false }); this.onActiveChange(null); onBlur?.(...args); }; getTreeNodeRequiredProps = () => { const { expandedKeys, selectedKeys, loadedKeys, loadingKeys, checkedKeys, halfCheckedKeys, dragOverNodeKey, dropPosition, keyEntities } = this.state; return { expandedKeys: expandedKeys || [], selectedKeys: selectedKeys || [], loadedKeys: loadedKeys || [], loadingKeys: loadingKeys || [], checkedKeys: checkedKeys || [], halfCheckedKeys: halfCheckedKeys || [], dragOverNodeKey, dropPosition, keyEntities: keyEntities }; }; // =========================== Expanded =========================== /** Set uncontrolled `expandedKeys`. This will also auto update `flattenNodes`. */ setExpandedKeys = expandedKeys => { const { treeData, fieldNames } = this.state; const flattenNodes = flattenTreeData(treeData, expandedKeys, fieldNames); this.setUncontrolledState({ expandedKeys, flattenNodes }, true); }; onNodeExpand = (e, treeNode) => { let { expandedKeys } = this.state; const { listChanging, fieldNames } = this.state; const { onExpand, loadData } = this.props; const { expanded } = treeNode; const key = treeNode[fieldNames.key]; // Do nothing when motion is in progress if (listChanging) { return; } // Update selected keys const certain = expandedKeys.includes(key); const targetExpanded = !expanded; warning(expanded && certain || !expanded && !certain, 'Expand state not sync with index check'); expandedKeys = targetExpanded ? arrAdd(expandedKeys, key) : arrDel(expandedKeys, key); this.setExpandedKeys(expandedKeys); onExpand?.(expandedKeys, { node: treeNode, expanded: targetExpanded, nativeEvent: e.nativeEvent }); // Async Load data if (targetExpanded && loadData) { const loadPromise = this.onNodeLoad(treeNode); if (loadPromise) { loadPromise.then(() => { // [Legacy] Refresh logic const newFlattenTreeData = flattenTreeData(this.state.treeData, expandedKeys, fieldNames); this.setUncontrolledState({ flattenNodes: newFlattenTreeData }); }).catch(() => { const { expandedKeys: currentExpandedKeys } = this.state; const expandedKeysToRestore = arrDel(currentExpandedKeys, key); this.setExpandedKeys(expandedKeysToRestore); }); } } }; onListChangeStart = () => { this.setUncontrolledState({ listChanging: true }); }; onListChangeEnd = () => { setTimeout(() => { this.setUncontrolledState({ listChanging: false }); }); }; // =========================== Keyboard =========================== onActiveChange = newActiveKey => { const { activeKey } = this.state; const { onActiveChange, itemScrollOffset = 0 } = this.props; if (activeKey === newActiveKey) { return; } this.setState({ activeKey: newActiveKey }); if (newActiveKey !== null) { this.scrollTo({ key: newActiveKey, offset: itemScrollOffset }); } onActiveChange?.(newActiveKey); }; getActiveItem = () => { const { activeKey, flattenNodes } = this.state; if (activeKey === null) { return null; } return flattenNodes.find(({ key }) => key === activeKey) || null; }; offsetActiveKey = offset => { const { flattenNodes, activeKey } = this.state; let index = flattenNodes.findIndex(({ key }) => key === activeKey); // Align with index if (index === -1 && offset < 0) { index = flattenNodes.length; } index = (index + offset + flattenNodes.length) % flattenNodes.length; const item = flattenNodes[index]; if (item) { const { key } = item; this.onActiveChange(key); } else { this.onActiveChange(null); } }; onKeyDown = event => { const { activeKey, expandedKeys, checkedKeys, fieldNames } = this.state; const { onKeyDown, checkable, selectable } = this.props; // >>>>>>>>>> Direction switch (event.which) { case KeyCode.UP: { this.offsetActiveKey(-1); event.preventDefault(); break; } case KeyCode.DOWN: { this.offsetActiveKey(1); event.preventDefault(); break; } } // >>>>>>>>>> Expand & Selection const activeItem = this.getActiveItem(); if (activeItem && activeItem.data) { const treeNodeRequiredProps = this.getTreeNodeRequiredProps(); const expandable = activeItem.data.isLeaf === false || !!(activeItem.data[fieldNames.children] || []).length; const eventNode = convertNodePropsToEventData({ ...getTreeNodeProps(activeKey, treeNodeRequiredProps), data: activeItem.data, active: true }); switch (event.which) { // >>> Expand case KeyCode.LEFT: { // Collapse if possible if (expandable && expandedKeys.includes(activeKey)) { this.onNodeExpand({}, eventNode); } else if (activeItem.parent) { this.onActiveChange(activeItem.parent.key); } event.preventDefault(); break; } case KeyCode.RIGHT: { // Expand if possible if (expandable && !expandedKeys.includes(activeKey)) { this.onNodeExpand({}, eventNode); } else if (activeItem.children && activeItem.children.length) { this.onActiveChange(activeItem.children[0].key); } event.preventDefault(); break; } // Selection case KeyCode.ENTER: case KeyCode.SPACE: { if (checkable && !eventNode.disabled && eventNode.checkable !== false && !eventNode.disableCheckbox) { this.onNodeCheck({}, eventNode, !checkedKeys.includes(activeKey)); } else if (!checkable && selectable && !eventNode.disabled && eventNode.selectable !== false) { this.onNodeSelect({}, eventNode); } break; } } } onKeyDown?.(event); }; /** * Only update the value which is not in props */ setUncontrolledState = (state, atomic = false, forceState = null) => { if (!this.destroyed) { let needSync = false; let allPassed = true; const newState = {}; Object.keys(state).forEach(name => { if (this.props.hasOwnProperty(name)) { allPassed = false; return; } needSync = true; newState[name] = state[name]; }); if (needSync && (!atomic || allPassed)) { this.setState({ ...newState, ...forceState }); } } }; scrollTo = scroll => { this.listRef.current.scrollTo(scroll); }; render() { const { focused, flattenNodes, keyEntities, draggingNodeKey, activeKey, dropLevelOffset, dropContainerKey, dropTargetKey, dropPosition, dragOverNodeKey, indent } = this.state; const { prefixCls, className, style, styles, classNames: treeClassNames, showLine, focusable, tabIndex = 0, selectable, showIcon, icon, switcherIcon, draggable, checkable, checkStrictly, disabled, motion, loadData, filterTreeNode, height, itemHeight, scrollWidth, virtual, titleRender, dropIndicatorRender, onContextMenu, onScroll, direction, rootClassName, rootStyle } = this.props; const domProps = pickAttrs(this.props, { aria: true, data: true }); // It's better move to hooks but we just simply keep here let draggableConfig; if (draggable) { if (typeof draggable === 'object') { draggableConfig = draggable; } else if (typeof draggable === 'function') { draggableConfig = { nodeDraggable: draggable }; } else { draggableConfig = {}; } } const contextValue = { styles, classNames: treeClassNames, prefixCls, selectable, showIcon, icon, switcherIcon, draggable: draggableConfig, draggingNodeKey, checkable, checkStrictly, disabled, keyEntities, dropLevelOffset, dropContainerKey, dropTargetKey, dropPosition, dragOverNodeKey, indent, direction, dropIndicatorRender, loadData, filterTreeNode, titleRender, onNodeClick: this.onNodeClick, onNodeDoubleClick: this.onNodeDoubleClick, onNodeExpand: this.onNodeExpand, onNodeSelect: this.onNodeSelect, onNodeCheck: this.onNodeCheck, onNodeLoad: this.onNodeLoad, onNodeMouseEnter: this.onNodeMouseEnter, onNodeMouseLeave: this.onNodeMouseLeave, onNodeContextMenu: this.onNodeContextMenu, onNodeDragStart: this.onNodeDragStart, onNodeDragEnter: this.onNodeDragEnter, onNodeDragOver: this.onNodeDragOver, onNodeDragLeave: this.onNodeDragLeave, onNodeDragEnd: this.onNodeDragEnd, onNodeDrop: this.onNodeDrop }; return /*#__PURE__*/React.createElement(TreeContext.Provider, { value: contextValue }, /*#__PURE__*/React.createElement("div", { className: classNames(prefixCls, className, rootClassName, { [`${prefixCls}-show-line`]: showLine, [`${prefixCls}-focused`]: focused, [`${prefixCls}-active-focused`]: activeKey !== null }), style: rootStyle }, /*#__PURE__*/React.createElement(NodeList, _extends({ ref: this.listRef, prefixCls: prefixCls, style: style, data: flattenNodes, disabled: disabled, selectable: selectable, checkable: !!checkable, motion: motion, dragging: draggingNodeKey !== null, height: height, itemHeight: itemHeight, virtual: virtual, focusable: focusable, focused: focused, tabIndex: tabIndex, activeItem: this.getActiveItem(), onFocus: this.onFocus, onBlur: this.onBlur, onKeyDown: this.onKeyDown, onActiveChange: this.onActiveChange, onListChangeStart: this.onListChangeStart, onListChangeEnd: this.onListChangeEnd, onContextMenu: onContextMenu, onScroll: onScroll, scrollWidth: scrollWidth }, this.getTreeNodeRequiredProps(), domProps)))); } } export default Tree;