import * as React from "react"; // import PropTypes from "prop-types"; import classNames from "classnames"; import { TreeContext } from "./context-types"; import { getDataAndAria, getDragNodesKeys, conductExpandParent, calcSelectedKeys, calcDropPosition, arrAdd, arrDel, posToArr } from "./util"; import { DataNode, IconType, Key, FlattenNode, DataEntity, EventDataNode, NodeInstance, ScrollTo } from "./interface"; import { flattenTreeData, convertTreeToData, convertDataToEntities, convertNodePropsToEventData } from "./util/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; // onKeyDown?: React.KeyboardEventHandler; 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; } 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; prevProps: TreeProps; } // const keyPropType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); class Tree extends React.Component { // static propTypes = { // prefixCls: PropTypes.string, // className: PropTypes.string, // style: PropTypes.object, // tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // children: PropTypes.any, // treeData: PropTypes.array, // Generate treeNode by children // showLine: PropTypes.bool, // showIcon: PropTypes.bool, // icon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), // selectable: PropTypes.bool, // disabled: PropTypes.bool, // multiple: PropTypes.bool, // draggable: PropTypes.bool, // defaultExpandParent: PropTypes.bool, // autoExpandParent: PropTypes.bool, // defaultExpandAll: PropTypes.bool, // defaultExpandedKeys: PropTypes.arrayOf(keyPropType), // expandedKeys: PropTypes.arrayOf(keyPropType), // defaultSelectedKeys: PropTypes.arrayOf(keyPropType), // selectedKeys: PropTypes.arrayOf(keyPropType), // onClick: PropTypes.func, // onDoubleClick: PropTypes.func, // onExpand: PropTypes.func, // onSelect: PropTypes.func, // onLoad: PropTypes.func, // loadData: PropTypes.func, // loadedKeys: PropTypes.arrayOf(keyPropType), // onMouseEnter: PropTypes.func, // onMouseLeave: PropTypes.func, // onRightClick: PropTypes.func, // onDragStart: PropTypes.func, // onDragEnter: PropTypes.func, // onDragOver: PropTypes.func, // onDragLeave: PropTypes.func, // onDragEnd: PropTypes.func, // onDrop: PropTypes.func, // filterTreeNode: PropTypes.func, // motion: PropTypes.object, // switcherIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]) // }; 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, 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; this.setState({ dragging: true, dragNodesKeys: getDragNodesKeys(eventKey, keyEntities), expandedKeys: arrDel(expandedKeys, eventKey) }); 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 } = this.state; const { onDragEnter } = this.props; const { pos, eventKey } = node.props; if (!this.dragNode) 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.setState({ expandedKeys: newExpandedKeys }); } if (onDragEnter) { onDragEnter({ event, node: convertNodePropsToEventData(node.props), expandedKeys: newExpandedKeys }); } }, 400); }, 0); }; onNodeDragOver = ( event: React.MouseEvent, node: NodeInstance ) => { const { onDragOver } = this.props; const { eventKey } = node.props; // 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: convertNodePropsToEventData(this.dragNode.props), 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 { treeData } = this.state; const { onExpand, loadData } = this.props; const { key, expanded } = treeNode; // Update selected keys const targetExpanded = !expanded; if (targetExpanded) { expandedKeys = arrAdd(expandedKeys, key); } else { expandedKeys = arrDel(expandedKeys, key); } const flattenNodes: FlattenNode[] = flattenTreeData(treeData, expandedKeys); this.setUncontrolledState({ expandedKeys, flattenNodes }, true); if (onExpand) { onExpand(expandedKeys, { node: treeNode, expanded: targetExpanded, nativeEvent: e.nativeEvent }); } // Async Load data if (targetExpanded && loadData) { const loadPromise = this.onNodeLoad(treeNode); return loadPromise ? loadPromise.then(() => { // [Legacy] Refresh logic const newFlattenTreeData = flattenTreeData( this.state.treeData, expandedKeys ); this.setUncontrolledState({ flattenNodes: newFlattenTreeData }); }) : null; } return null; }; 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 onActiveChange = (activeKey: Key) => { const { onActiveChange } = this.props; this.setState({ activeKey }); if (activeKey !== null) { this.scrollTo({ key: activeKey }); } if (onActiveChange) { onActiveChange(activeKey); } }; getActiveItem = () => { const { activeKey, flattenNodes } = this.state; if (activeKey === null) { return null; } return flattenNodes.find(({ data: { key } }) => key === activeKey) || null; }; /** * Only update the value which is not in props */ setUncontrolledState = ( state: Partial, atomic: boolean = false ) => { 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); } }; 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 } = this.props; const domProps: React.HTMLAttributes = getDataAndAria( this.props ); return (
); } } export { TreeProps }; export default Tree;