import * as React from "react"; import classNames from "classnames"; import { TreeContext } from "./context-types"; import { getDataAndAria, getDragNodesKeys, conductExpandParent, calcSelectedKeys, calcDropPosition, arrAdd, arrDel, posToArr, } from "./utils/common-util"; import { DataNode, IconType, Key, FlattenNode, DataEntity, EventDataNode, NodeInstance, ScrollTo, } from "./interface"; import { flattenTreeData, convertTreeToData, convertDataToEntities, convertNodePropsToEventData, } from "./utils/tree-util"; import { NodeList, MOTION_KEY, MotionEntity, NodeListRef } from "./node-list"; import TreeNode from "./tree-node"; interface TreeProps { prefixCls: string; className?: string; style?: React.CSSProperties; focusable?: boolean; tabIndex?: number; children?: React.ReactNode; treeData?: DataNode[]; // Generate treeNode by children showLine?: boolean; showIcon?: boolean; icon?: IconType; selectable?: boolean; disabled?: boolean; multiple?: boolean; draggable?: boolean; defaultExpandParent?: boolean; autoExpandParent?: boolean; defaultExpandAll?: boolean; defaultExpandedKeys?: Key[]; expandedKeys?: Key[]; defaultCheckedKeys?: Key[]; defaultSelectedKeys?: Key[]; selectedKeys?: Key[]; onFocus?: React.FocusEventHandler; onBlur?: React.FocusEventHandler; onClick?: (e: React.MouseEvent, treeNode: EventDataNode) => void; onDoubleClick?: (e: React.MouseEvent, treeNode: EventDataNode) => void; onExpand?: ( expandedKeys: Key[], info: { node: EventDataNode; expanded: boolean; nativeEvent: MouseEvent; } ) => void; onSelect?: ( selectedKeys: Key[], info: { event: "select"; selected: boolean; node: EventDataNode; selectedNodes: DataNode[]; nativeEvent: MouseEvent; } ) => void; onLoad?: ( loadedKeys: Key[], info: { event: "load"; node: EventDataNode; } ) => void; loadData?: (treeNode: EventDataNode) => Promise; loadedKeys?: Key[]; onMouseEnter?: (info: { event: React.MouseEvent; node: EventDataNode; }) => void; onMouseLeave?: (info: { event: React.MouseEvent; node: EventDataNode; }) => void; onRightClick?: (info: { event: React.MouseEvent; node: EventDataNode; }) => void; onDragStart?: (info: { event: React.MouseEvent; node: EventDataNode; }) => void; onDragEnter?: (info: { event: React.MouseEvent; node: EventDataNode; expandedKeys: Key[]; }) => void; onDragOver?: (info: { event: React.MouseEvent; node: EventDataNode }) => void; onDragLeave?: (info: { event: React.MouseEvent; node: EventDataNode; }) => void; onDragEnd?: (info: { event: React.MouseEvent; node: EventDataNode }) => void; onDrop?: (info: { event: React.MouseEvent; node: EventDataNode; dragNode: EventDataNode; dragNodesKeys: Key[]; dropPosition: number; dropToGap: boolean; }) => void; /** * Do not use in your production code directly since this will be refactor. */ onActiveChange?: (key: Key) => void; filterTreeNode?: (treeNode: EventDataNode) => boolean; motion?: any; switcherIcon?: IconType; // Virtual List height?: number; itemHeight?: number; virtual?: boolean; } interface TreeState { keyEntities: Record; selectedKeys: Key[]; loadedKeys: Key[]; loadingKeys: Key[]; expandedKeys: Key[]; dragging: boolean; dragNodesKeys: Key[]; dragOverNodeKey: Key; dropPosition: number; treeData: DataNode[]; flattenNodes: FlattenNode[]; focused: boolean; activeKey: Key; // Record if list is changing listChanging: boolean; prevProps: TreeProps; } class Tree extends React.Component { static defaultProps = { prefixCls: "co-tree", showLine: false, showIcon: true, selectable: true, multiple: false, disabled: false, draggable: false, defaultExpandParent: true, autoExpandParent: false, defaultExpandAll: false, defaultExpandedKeys: [], defaultSelectedKeys: [], }; static TreeNode = TreeNode; delayedDragEnterLogic: Record; state: TreeState = { keyEntities: {}, selectedKeys: [], loadedKeys: [], loadingKeys: [], expandedKeys: [], dragging: false, dragNodesKeys: [], dragOverNodeKey: null, dropPosition: null, treeData: [], flattenNodes: [], focused: false, activeKey: null, listChanging: false, prevProps: null, }; dragNode: NodeInstance; listRef = React.createRef(); static getDerivedStateFromProps(props: TreeProps, prevState: TreeState) { const { prevProps } = prevState; //用之前的静态state表示旧props const newState: Partial = { prevProps: props, }; //定义一个新的state(取自旧state),再将其指定给props(函数原始继承的props) const needSync = (name: string) => { return ( (!prevProps && name in props) || (prevProps && prevProps[name] !== props[name]) ); }; // ================== Tree Node ================== let treeData: DataNode[]; // Check if `treeData` or `children` changed and save into the state. if (needSync("treeData")) { ({ treeData } = props); } else if (needSync("children")) { treeData = convertTreeToData(props.children); } // Save flatten nodes info and convert `treeData` into keyEntities if (treeData) { newState.treeData = treeData; const entitiesMap = convertDataToEntities(treeData); newState.keyEntities = { [MOTION_KEY]: MotionEntity, ...entitiesMap.keyEntities, }; } 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]; newState.expandedKeys = Object.keys(cloneKeyEntities).map( (key) => cloneKeyEntities[key].key ); } 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: FlattenNode[] = flattenTreeData( treeData || prevState.treeData, newState.expandedKeys || prevState.expandedKeys ); 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 ); } } // ================= loadedKeys ================== if (needSync("loadedKeys")) { newState.loadedKeys = props.loadedKeys; } return newState; } onNodeDragStart = ( event: React.MouseEvent, node: NodeInstance ) => { const { expandedKeys, keyEntities } = this.state; const { onDragStart } = this.props; const { eventKey } = node.props; this.dragNode = node; const newExpandedKeys = arrDel(expandedKeys, eventKey); this.setState({ dragging: true, dragNodesKeys: getDragNodesKeys(eventKey, keyEntities), }); this.setExpandedKeys(newExpandedKeys); if (onDragStart) { onDragStart({ event, node: convertNodePropsToEventData(node.props) }); } }; /** * Select handler is less small 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: React.MouseEvent, node: NodeInstance ) => { const { expandedKeys, keyEntities, dragNodesKeys } = this.state; const { onDragEnter } = this.props; const { pos, eventKey } = node.props; if (!this.dragNode || dragNodesKeys.indexOf(eventKey) !== -1) return; const dropPosition = calcDropPosition(event, node); // Skip if drag node is self if (this.dragNode.props.eventKey === eventKey && dropPosition === 0) { this.setState({ dragOverNodeKey: "", dropPosition: null, }); return; } // Add timeout to let onDragLevel fire before onDragEnter, // so that we can clean drag props for onDragLeave node. // Macro task for this: setTimeout(() => { // Update drag over node this.setState({ dragOverNodeKey: eventKey, dropPosition, }); // Side effect for delay drag if (!this.delayedDragEnterLogic) { this.delayedDragEnterLogic = {}; } Object.keys(this.delayedDragEnterLogic).forEach((key) => { clearTimeout(this.delayedDragEnterLogic[key]); }); this.delayedDragEnterLogic[pos] = window.setTimeout(() => { if (!this.state.dragging) return; let newExpandedKeys = [...expandedKeys]; const entity = keyEntities[eventKey]; if (entity && (entity.children || []).length) { newExpandedKeys = arrAdd(expandedKeys, eventKey); } if (!("expandedKeys" in this.props)) { this.setExpandedKeys(newExpandedKeys); } if (onDragEnter) { onDragEnter({ event, node: convertNodePropsToEventData(node.props), expandedKeys: newExpandedKeys, }); } }, 400); }, 0); }; onNodeDragOver = ( event: React.MouseEvent, node: NodeInstance ) => { const { dragNodesKeys } = this.state; const { onDragOver } = this.props; const { eventKey } = node.props; if (dragNodesKeys.indexOf(eventKey) !== -1) { return; } // Update drag position if (this.dragNode && eventKey === this.state.dragOverNodeKey) { const dropPosition = calcDropPosition(event, node); if (dropPosition === this.state.dropPosition) return; this.setState({ dropPosition, }); } if (onDragOver) { onDragOver({ event, node: convertNodePropsToEventData(node.props) }); } }; onNodeDragLeave = ( event: React.MouseEvent, node: NodeInstance ) => { const { onDragLeave } = this.props; this.setState({ dragOverNodeKey: "", }); if (onDragLeave) { onDragLeave({ event, node: convertNodePropsToEventData(node.props) }); } }; onNodeDragEnd = ( event: React.MouseEvent, node: NodeInstance ) => { const { onDragEnd } = this.props; this.setState({ dragOverNodeKey: "", }); this.cleanDragState(); if (onDragEnd) { onDragEnd({ event, node: convertNodePropsToEventData(node.props) }); } this.dragNode = null; }; onNodeDrop = ( event: React.MouseEvent, node: NodeInstance ) => { const { dragNodesKeys = [], dropPosition } = this.state; const { onDrop } = this.props; const { eventKey, pos } = node.props; this.setState({ dragOverNodeKey: "", }); this.cleanDragState(); if (dragNodesKeys.indexOf(eventKey) !== -1) { return; } const posArr = posToArr(pos); const dropResult = { event, node: convertNodePropsToEventData(node.props), dragNode: this.dragNode ? convertNodePropsToEventData(this.dragNode.props) : null, dragNodesKeys: dragNodesKeys.slice(), dropPosition: dropPosition + Number(posArr[posArr.length - 1]), dropToGap: false, }; if (dropPosition !== 0) { dropResult.dropToGap = true; } if (onDrop) { onDrop(dropResult); } this.dragNode = null; }; cleanDragState = () => { const { dragging } = this.state; if (dragging) { this.setState({ dragging: false, }); } }; onNodeClick = ( e: React.MouseEvent, treeNode: EventDataNode ) => { const { onClick } = this.props; if (onClick) { onClick(e, treeNode); } }; onNodeDoubleClick = ( e: React.MouseEvent, treeNode: EventDataNode ) => { const { onDoubleClick } = this.props; if (onDoubleClick) { onDoubleClick(e, treeNode); } }; onNodeSelect = ( e: React.MouseEvent, treeNode: EventDataNode ) => { let { selectedKeys } = this.state; const { keyEntities } = this.state; const { onSelect, multiple } = this.props; const { selected, key } = treeNode; const targetSelected = !selected; // Update selected keys if (!targetSelected) { selectedKeys = arrDel(selectedKeys, key); } else if (!multiple) { selectedKeys = [key]; } else { selectedKeys = arrAdd(selectedKeys, key); } //Not found related usage in doc or upper libs const selectedNodes = selectedKeys .map((selectedKey) => { const entity = keyEntities[selectedKey]; if (!entity) return null; return entity.node; }) .filter((node) => node); this.setUncontrolledState({ selectedKeys }); if (onSelect) { onSelect(selectedKeys, { event: "select", selected: targetSelected, node: treeNode, selectedNodes, nativeEvent: e.nativeEvent, }); } }; onNodeLoad = (treeNode: EventDataNode) => new Promise((resolve) => { // We need to get the latest state of loading/loaded keys this.setState(({ loadedKeys = [], loadingKeys = [] }): any => { const { loadData, onLoad } = this.props; const { key } = treeNode; if ( !loadData || loadedKeys.indexOf(key) !== -1 || loadingKeys.indexOf(key) !== -1 ) { // react 15 will warn if return null return {}; } // Process load data const promise = loadData(treeNode); promise.then(() => { const { loadedKeys: currentLoadedKeys, loadingKeys: currentLoadingKeys, } = this.state; const newLoadedKeys = arrAdd(currentLoadedKeys, key); const newLoadingKeys = arrDel(currentLoadingKeys, key); // onLoad should trigger before internal setState to avoid `loadData` trigger twice. if (onLoad) { onLoad(newLoadedKeys, { event: "load", node: treeNode, }); } this.setUncontrolledState({ loadedKeys: newLoadedKeys, }); this.setState({ loadingKeys: newLoadingKeys, }); resolve(); }); return { loadingKeys: arrAdd(loadingKeys, key), }; }); }); onNodeExpand = ( e: React.MouseEvent, treeNode: EventDataNode ) => { let { expandedKeys } = this.state; const { listChanging } = this.state; const { onExpand, loadData } = this.props; const { key, expanded } = treeNode; if (listChanging) { return; } // Update selected keys const targetExpanded = !expanded; if (targetExpanded) { expandedKeys = arrAdd(expandedKeys, key); } else { expandedKeys = arrDel(expandedKeys, key); } this.setExpandedKeys(expandedKeys); if (onExpand) { 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 ); this.setUncontrolledState({ flattenNodes: newFlattenTreeData }); }); } } }; onNodeMouseEnter = ( event: React.MouseEvent, node: EventDataNode ) => { const { onMouseEnter } = this.props; if (onMouseEnter) { onMouseEnter({ event, node }); } }; onNodeMouseLeave = ( event: React.MouseEvent, node: EventDataNode ) => { const { onMouseLeave } = this.props; if (onMouseLeave) { onMouseLeave({ event, node }); } }; onNodeContextMenu = ( event: React.MouseEvent, node: EventDataNode ) => { const { onRightClick } = this.props; if (onRightClick) { event.preventDefault(); onRightClick({ event, node }); } }; onFocus: React.FocusEventHandler = (...args) => { const { onFocus } = this.props; this.setState({ focused: true }); if (onFocus) { onFocus(...args); } }; onBlur: React.FocusEventHandler = (...args) => { const { onBlur } = this.props; this.setState({ focused: false }); this.onActiveChange(null); if (onBlur) { onBlur(...args); } }; getTreeNodeRequiredProps = () => { const { expandedKeys, selectedKeys, loadedKeys, loadingKeys, dragOverNodeKey, dropPosition, keyEntities, } = this.state; return { expandedKeys: expandedKeys || [], selectedKeys: selectedKeys || [], loadedKeys: loadedKeys || [], loadingKeys: loadingKeys || [], dragOverNodeKey, dropPosition, keyEntities, }; }; // spliting line setExpandedKeys = (expandedKeys: Key[]) => { const { treeData } = this.state; const flattenNodes: FlattenNode[] = flattenTreeData(treeData, expandedKeys); this.setUncontrolledState( { expandedKeys, flattenNodes, }, true ); }; onListChangeStart = () => { this.setUncontrolledState({ listChanging: true, }); }; onListChangeEnd = () => { setTimeout(() => { this.setUncontrolledState({ listChanging: false, }); }); }; onActiveChange = (newActiveKey: Key) => { const { activeKey } = this.state; const { onActiveChange } = this.props; if (activeKey === newActiveKey) { return; } this.setState({ activeKey: newActiveKey }); if (newActiveKey !== null) { this.scrollTo({ key: newActiveKey }); } if (onActiveChange) { onActiveChange(newActiveKey); } }; getActiveItem = () => { const { activeKey, flattenNodes } = this.state; if (activeKey === null) { return null; } return flattenNodes.find(({ data: { key } }) => key === activeKey) || null; }; offsetActiveKey = (offset: number) => { const { flattenNodes, activeKey } = this.state; let index = flattenNodes.findIndex( ({ data: { 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.data; this.onActiveChange(key); } else { this.onActiveChange(null); } }; /** * Only update the value which is not in props */ setUncontrolledState = ( state: Partial, atomic: boolean = false, forceState: Partial | null = null ) => { let needSync = false; let allPassed = true; const newState = {}; Object.keys(state).forEach((name) => { if (name in this.props) { allPassed = false; return; } needSync = true; newState[name] = state[name]; }); if (needSync && (!atomic || allPassed)) { this.setState({ ...newState, ...forceState, } as TreeState); } }; scrollTo: ScrollTo = (scroll) => { this.listRef.current.scrollTo(scroll); }; render() { const { focused, flattenNodes, keyEntities, dragging, activeKey, } = this.state; const { prefixCls, className, style, showLine, focusable, tabIndex = 0, selectable, showIcon, icon, switcherIcon, draggable, disabled, motion, loadData, filterTreeNode, height, itemHeight, virtual, } = this.props; const domProps: React.HTMLAttributes = getDataAndAria( this.props ); return (
); } } export { TreeProps }; export default Tree;