UNPKG

@rc-component/tree

Version:
1,302 lines (1,261 loc) 39.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _clsx = require("clsx"); var _util = require("@rc-component/util"); var React = _interopRequireWildcard(require("react")); var _contextTypes = require("./contextTypes"); var _DropIndicator = _interopRequireDefault(require("./DropIndicator")); var _NodeList = _interopRequireWildcard(require("./NodeList")); var _TreeNode = _interopRequireDefault(require("./TreeNode")); var _util2 = require("./util"); var _conductUtil = require("./utils/conductUtil"); var _keyUtil = _interopRequireDefault(require("./utils/keyUtil")); var _treeUtil = require("./utils/treeUtil"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } 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: Fully accessibility support // Reference: https://www.w3.org/WAI/ARIA/apg/patterns/treeview 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.default, allowDrop: () => true, expandAction: false }; static TreeNode = _TreeNode.default; 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: [], activeKey: null, listChanging: false, prevProps: null, fieldNames: (0, _treeUtil.fillFieldNames)() }; dragStartMousePosition = null; dragNodeProps = null; currentMouseOverDroppableNodeKey = null; focusedByMouse = false; listRef = /*#__PURE__*/React.createRef(); componentDidMount() { this.destroyed = false; this.onUpdated(); window.addEventListener('mouseup', this.onGlobalMouseUp); } 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); window.removeEventListener('mouseup', this.onGlobalMouseUp); 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 = (0, _treeUtil.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')) { (0, _util.warning)(false, '`children` of Tree is deprecated. Please use `treeData` instead.'); treeData = (0, _treeUtil.convertTreeToData)(props.children); } // Save flatten nodes info and convert `treeData` into keyEntities if (treeData) { newState.treeData = treeData; const entitiesMap = (0, _treeUtil.convertDataToEntities)(treeData, { fieldNames }); newState.keyEntities = { [_NodeList.MOTION_KEY]: _NodeList.MotionEntity, ...entitiesMap.keyEntities }; // Warning if treeNode not provide key if (process.env.NODE_ENV !== 'production') { (0, _treeUtil.warningWithoutKey)(treeData, fieldNames); } } const keyEntities = newState.keyEntities || prevState.keyEntities; // ================ expandedKeys ================= if (needSync('expandedKeys') || prevProps && needSync('autoExpandParent')) { newState.expandedKeys = props.autoExpandParent || !prevProps && props.defaultExpandParent ? (0, _util2.conductExpandParent)(props.expandedKeys, keyEntities) : props.expandedKeys; } else if (!prevProps && props.defaultExpandAll) { const cloneKeyEntities = { ...keyEntities }; delete cloneKeyEntities[_NodeList.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 ? (0, _util2.conductExpandParent)(props.defaultExpandedKeys, keyEntities) : props.defaultExpandedKeys; } if (!newState.expandedKeys) { delete newState.expandedKeys; } // ================ flattenNodes ================= if (treeData || newState.expandedKeys) { const flattenNodes = (0, _treeUtil.flattenTreeData)(treeData || prevState.treeData, newState.expandedKeys || prevState.expandedKeys, fieldNames); newState.flattenNodes = flattenNodes; } // ================ selectedKeys ================= if (props.selectable) { if (needSync('selectedKeys')) { newState.selectedKeys = (0, _util2.calcSelectedKeys)(props.selectedKeys, props); } else if (!prevProps && props.defaultSelectedKeys) { newState.selectedKeys = (0, _util2.calcSelectedKeys)(props.defaultSelectedKeys, props); } } // ================= checkedKeys ================= if (props.checkable) { let checkedKeyEntity; if (needSync('checkedKeys')) { checkedKeyEntity = (0, _util2.parseCheckedKeys)(props.checkedKeys) || {}; } else if (!prevProps && props.defaultCheckedKeys) { checkedKeyEntity = (0, _util2.parseCheckedKeys)(props.defaultCheckedKeys) || {}; } else if (treeData) { // If `treeData` changed, we also need check it checkedKeyEntity = (0, _util2.parseCheckedKeys)(props.checkedKeys) || { checkedKeys: prevState.checkedKeys, halfCheckedKeys: prevState.halfCheckedKeys }; } if (checkedKeyEntity) { let { checkedKeys = [], halfCheckedKeys = [] } = checkedKeyEntity; if (!props.checkStrictly) { const conductKeys = (0, _conductUtil.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 = (0, _util2.arrDel)(expandedKeys, eventKey); this.setState({ draggingNodeKey: eventKey, dragChildrenKeys: (0, _util2.getDragChildrenKeys)(eventKey, keyEntities), indent: this.listRef.current.getIndentWidth() }); this.setExpandedKeys(newExpandedKeys); window.addEventListener('dragend', this.onWindowDragEnd); onDragStart?.({ event, node: (0, _treeUtil.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 } = (0, _util2.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 = (0, _keyUtil.default)(keyEntities, nodeProps.eventKey); if (entity && (entity.children || []).length) { newExpandedKeys = (0, _util2.arrAdd)(expandedKeys, nodeProps.eventKey); } if (!this.props.hasOwnProperty('expandedKeys')) { this.setExpandedKeys(newExpandedKeys); } onExpand?.(newExpandedKeys, { node: (0, _treeUtil.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: (0, _treeUtil.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 } = (0, _util2.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: (0, _treeUtil.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: (0, _treeUtil.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: (0, _treeUtil.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 = { ...(0, _treeUtil.getTreeNodeProps)(dropTargetKey, this.getTreeNodeRequiredProps()), active: this.getActiveItem()?.key === dropTargetKey, data: (0, _keyUtil.default)(this.state.keyEntities, dropTargetKey).node }; const dropToChild = dragChildrenKeys.includes(dropTargetKey); (0, _util.warning)(!dropToChild, "Can not drop to dragNode's children node. This is a bug of rc-tree. Please report an issue."); const posArr = (0, _util2.posToArr)(dropTargetPos); const dropResult = { event, node: (0, _treeUtil.convertNodePropsToEventData)(abstractDropNodeProps), dragNode: this.dragNodeProps ? (0, _treeUtil.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 = (0, _treeUtil.convertNodePropsToEventData)({ ...(0, _treeUtil.getTreeNodeProps)(key, this.getTreeNodeRequiredProps()), data: node.data }); this.setExpandedKeys(expanded ? (0, _util2.arrDel)(expandedKeys, key) : (0, _util2.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 = (0, _util2.arrDel)(selectedKeys, key); } else if (!multiple) { selectedKeys = [key]; } else { selectedKeys = (0, _util2.arrAdd)(selectedKeys, key); } // [Legacy] Not found related usage in doc or upper libs const selectedNodes = selectedKeys.map(selectedKey => { const entity = (0, _keyUtil.default)(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 ? (0, _util2.arrAdd)(oriCheckedKeys, key) : (0, _util2.arrDel)(oriCheckedKeys, key); const halfCheckedKeys = (0, _util2.arrDel)(oriHalfCheckedKeys, key); checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys }; eventObj.checkedNodes = checkedKeys.map(checkedKey => (0, _keyUtil.default)(keyEntities, checkedKey)).filter(Boolean).map(entity => entity.node); this.setUncontrolledState({ checkedKeys }); } else { // Always fill first let { checkedKeys, halfCheckedKeys } = (0, _conductUtil.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 } = (0, _conductUtil.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 = (0, _keyUtil.default)(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 = (0, _keyUtil.default)(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 = (0, _util2.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: (0, _util2.arrDel)(prevState.loadingKeys, key) })); resolve(); }).catch(e => { this.setState(prevState => ({ loadingKeys: (0, _util2.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; (0, _util.warning)(false, 'Retry for `loadData` many times but still failed. No more retry.'); this.setUncontrolledState({ loadedKeys: (0, _util2.arrAdd)(currentLoadedKeys, key) }); resolve(); } reject(e); }); return { loadingKeys: (0, _util2.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 }); } }; onMouseDown = event => { this.focusedByMouse = true; const { onMouseDown } = this.props; onMouseDown?.(event); }; onGlobalMouseUp = () => { this.focusedByMouse = false; }; onFocus = (...args) => { const { onFocus, disabled } = this.props; const { activeKey, selectedKeys, flattenNodes } = this.state; if (!this.focusedByMouse && !disabled && activeKey === null) { const visibleSelectedKey = selectedKeys.find(key => { return flattenNodes.some(nodeItem => nodeItem.key === key); }); if (visibleSelectedKey !== undefined) { this.onActiveChange(visibleSelectedKey); } else { this.onActiveChange(flattenNodes?.[0]?.key || null); } } onFocus?.(...args); }; onBlur = (...args) => { const { onBlur } = this.props; 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 = (0, _treeUtil.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; (0, _util.warning)(expanded && certain || !expanded && !certain, 'Expand state not sync with index check'); expandedKeys = targetExpanded ? (0, _util2.arrAdd)(expandedKeys, key) : (0, _util2.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 = (0, _treeUtil.flattenTreeData)(this.state.treeData, expandedKeys, fieldNames); this.setUncontrolledState({ flattenNodes: newFlattenTreeData }); }).catch(() => { const { expandedKeys: currentExpandedKeys } = this.state; const expandedKeysToRestore = (0, _util2.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, flattenNodes, keyEntities } = this.state; const { onKeyDown, checkable, selectable, disabled, loadData } = this.props; if (disabled) { return; } // >>>>>>>>>> Direction switch (event.key) { case 'ArrowUp': { this.offsetActiveKey(-1); event.preventDefault(); break; } case 'ArrowDown': { this.offsetActiveKey(1); event.preventDefault(); break; } case 'Home': { this.onActiveChange(flattenNodes?.[0]?.key); event.preventDefault(); break; } case 'End': { this.onActiveChange(flattenNodes?.[flattenNodes.length - 1]?.key); event.preventDefault(); break; } } // >>>>>>>>>> Expand & Selection const activeItem = this.getActiveItem(); if (activeItem && activeItem.data) { const treeNodeRequiredProps = this.getTreeNodeRequiredProps(); const eventNode = (0, _treeUtil.convertNodePropsToEventData)({ ...(0, _treeUtil.getTreeNodeProps)(activeKey, treeNodeRequiredProps), data: activeItem.data, active: true }); const entity = (0, _keyUtil.default)(keyEntities, activeKey); const hasChildren = !!entity?.children?.length; const expandable = !(0, _treeUtil.isLeafNode)(activeItem.data.isLeaf, loadData, hasChildren, eventNode.loaded); const canCheck = checkable && !eventNode.disabled && eventNode.checkable !== false && !eventNode.disableCheckbox; const canSelect = !checkable && selectable && !eventNode.disabled && eventNode.selectable !== false; switch (event.key) { // >>> Expand case 'ArrowLeft': { // Collapse if possible if (expandable && expandedKeys.includes(activeKey)) { this.onNodeExpand({}, eventNode); } else if (activeItem.parent) { this.onActiveChange(activeItem.parent.key); } event.preventDefault(); break; } case 'ArrowRight': { // 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; } case 'Enter': { if (expandable) { event.preventDefault(); this.onNodeExpand({}, eventNode); } else if (canCheck) { if (!checkedKeys.includes(activeKey)) { event.preventDefault(); this.onNodeCheck({}, eventNode, true); } } else if (canSelect && !eventNode.selected) { event.preventDefault(); this.onNodeSelect({}, eventNode); } break; } case ' ': { if (canCheck) { event.preventDefault(); this.onNodeCheck({}, eventNode, !checkedKeys.includes(activeKey)); } else if (canSelect) { event.preventDefault(); 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 { flattenNodes, keyEntities, draggingNodeKey, 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 = (0, _util.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(_contextTypes.TreeContext.Provider, { value: contextValue }, /*#__PURE__*/React.createElement("div", { className: (0, _clsx.clsx)(prefixCls, className, rootClassName, { [`${prefixCls}-show-line`]: showLine }), style: rootStyle }, /*#__PURE__*/React.createElement(_NodeList.default, _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, tabIndex: tabIndex, activeItem: this.getActiveItem(), onFocus: this.onFocus, onMouseDown: this.onMouseDown, onBlur: this.onBlur, onKeyDown: this.onKeyDown, onActiveChange: this.onActiveChange, onListChangeStart: this.onListChangeStart, onListChangeEnd: this.onListChangeEnd, onContextMenu: onContextMenu, onScroll: onScroll, scrollWidth: scrollWidth }, this.getTreeNodeRequiredProps(), domProps)))); } } var _default = exports.default = Tree;