import * as React from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; //@ts-ignore import { DataNode, IconType, Key, EventDataNode, DataEntity, FlattenNode, NodeInstance, ScrollTo } from "./interface"; import { getDragNodesKeys, arrDel, calcDropPosition, arrAdd, posToArr, getDataAndAria, conductExpandParent, calcSelectedKeys } from "./util"; import { convertNodePropsToEventData, flattenTreeData, convertTreeToData, convertDataToEntities } from "./util/tree-util"; import { NodeList, MOTION_KEY, MotionEntity, NodeListRef } from "./node-list"; import { TreeContext } from "./context-types"; 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; //extra state-props keyEntities: Record; } 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]); const Tree: React.SFC = props => { Tree.propTypes = { prefixCls: PropTypes.string, className: PropTypes.string, style: PropTypes.object, tabIndex: PropTypes.number, children: PropTypes.any, treeData: PropTypes.array, 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]) }; Tree.defaultProps = { prefixCls: "co-tree", showLine: false, showIcon: true, selectable: true, multiple: false, disabled: false, draggable: false, defaultExpandParent: true, autoExpandParent: false, defaultExpandAll: false, defaultExpandedKeys: [], defaultSelectedKeys: [] }; // const [keyEntities, setKeyEntities] = React.useState(); const [selectedKeys, setSelectedKeys] = React.useState([]); const [loadedKeys, setLoadedKeys] = React.useState([]); const [loadingKeys, setLoadingKeys] = React.useState([]); const [expandedKeys, setExpandedKeys] = React.useState([]); const [dragging, setDragging] = React.useState(false); const [dragNodesKeys, setDragNodesKeys] = React.useState([]); const [dragOverNodeKey, setDragOverNodeKey] = React.useState(); const [dropPosition, setDropPosition] = React.useState(null); const [treeData, setTreeData] = React.useState([]); const [flattenNodes, setFlattenNode] = React.useState([]); const [focused, setFocused] = React.useState(false); const [activeKey, setActiveKey] = React.useState(null); // const [prevProps, setPrevProps] = React.useState(null); let dragNode: NodeInstance; let delayedDragEnterLogic: Record; let listRef = React.createRef(); React.useCallback(() => { let prevProps: TreeState; const newState: Partial = { prevProps: props }; const needSync = (name: string) => { return ( (!prevProps && name in props) || (prevProps && prevProps[name] !== props[name]) ); }; if (needSync("treeData")) { setTreeData([]); } else if (needSync("children")) { setTreeData(convertTreeToData(props.children)); } if (treeData) { newState.treeData = treeData; const entitiesMap = convertDataToEntities(treeData); newState.keyEntities = { [MOTION_KEY]: MotionEntity, ...entitiesMap.keyEntities }; } const keyEntity = newState.keyEntities || props.keyEntities; if ( needSync("expandedKeys") || (prevProps && needSync("autoExpandParent")) ) { setExpandedKeys( props.autoExpandParent || (!prevProps && props.defaultExpandParent) ? conductExpandParent(props.expandedKeys, keyEntity) : props.expandedKeys ); } else if (!prevProps && props.defaultExpandAll) { const cloneKeyEntities = { ...keyEntity }; delete cloneKeyEntities[MOTION_KEY]; setExpandedKeys( Object.keys(cloneKeyEntities).map(key => cloneKeyEntities[key].key) ); } else if (!prevProps && props.defaultExpandedKeys) { setExpandedKeys( props.autoExpandParent || props.defaultExpandedKeys ? conductExpandParent(props.defaultExpandedKeys, keyEntity) : props.defaultExpandedKeys ); } if (!newState.expandedKeys) { delete newState.expandedKeys; } if (treeData || newState.expandedKeys) { const flattenNodes: FlattenNode[] = flattenTreeData( treeData, expandedKeys ); setFlattenNode(flattenNodes); } if (props.selectable) { if (needSync("selectedKeys")) { setSelectedKeys(calcSelectedKeys(props.selectedKeys, props)); } else if (!prevProps && props.defaultSelectedKeys) { setSelectedKeys(calcSelectedKeys(props.defaultExpandedKeys, props)); } } if (needSync("loadedKeys")) { setLoadedKeys(props.loadedKeys); } }, null); const onNodeDragStart = ( event: React.MouseEvent, node: NodeInstance ) => { const keyEntities = props.keyEntities; const { onDragStart } = props; const { eventKey } = node.props; setDragging(true); setDragNodesKeys(() => getDragNodesKeys(eventKey, keyEntities)), setExpandedKeys(() => arrDel(expandedKeys, eventKey)); if (onDragStart) { onDragStart({ event, node: convertNodePropsToEventData(node.props) }); } }; const onNodeDragEnter = ( event: React.MouseEvent, node: NodeInstance ) => { const keyEntities = props.keyEntities; const { onDragEnter } = props; const { pos, eventKey } = node.props; if (!dragNode) return; const dropPosition = calcDropPosition(event, node); //Skip if drag node is self if (dragNode.props.eventKey === eventKey && dropPosition === 0) { setDragOverNodeKey(""), setDropPosition(null); return; } setTimeout(() => { setDragOverNodeKey(eventKey), setDropPosition(null); if (!delayedDragEnterLogic) { delayedDragEnterLogic = {}; } Object.keys(delayedDragEnterLogic).forEach(key => { clearTimeout(delayedDragEnterLogic[key]); }); delayedDragEnterLogic[pos] = window.setTimeout(() => { if (dragging) return; let newExpandedKeys = [...expandedKeys]; const entity = keyEntities[eventKey]; if (entity && (entity.children || []).length) { newExpandedKeys = arrAdd(expandedKeys, eventKey); } if (!("expandedKeys" in props)) { setExpandedKeys(newExpandedKeys); } if (onDragEnter) { onDragEnter({ event, node: convertNodePropsToEventData(node.props), expandedKeys: newExpandedKeys }); } }, 400); }, 0); }; const onNodeDragOver = ( event: React.MouseEvent, node: NodeInstance ) => { const { onDragOver } = props; const { eventKey } = node.props; //Update drag position if (dragNode && eventKey === dragOverNodeKey) { const $dropPosition = calcDropPosition(event, node); if ($dropPosition === dropPosition) return; setDropPosition(dropPosition); } if (onDragOver) { onDragOver({ event, node: convertNodePropsToEventData(node.props) }); } }; const onNodeDragLeave = ( event: React.MouseEvent, node: NodeInstance ) => { const { onDragLeave } = props; setDragOverNodeKey(""); if (onDragLeave) { onDragLeave({ event, node: convertNodePropsToEventData(node.props) }); } }; const onNodeDragEnd = ( event: React.MouseEvent, node: NodeInstance ) => { const { onDragEnd } = props; setDragOverNodeKey(""); cleanDragState(); if (onDragEnd) { onDragEnd({ event, node: convertNodePropsToEventData(node.props) }); } dragNode = null; }; const onNodeDrop = ( event: React.MouseEvent, node: NodeInstance ) => { const { onDrop } = props; const { eventKey, pos } = node.props; setDragOverNodeKey(""); cleanDragState(); if (dragNodesKeys.indexOf(eventKey) !== -1) { return; } const posArr = posToArr(pos); const dropResult = { event, node: convertNodePropsToEventData(node.props), dragNode: convertNodePropsToEventData(dragNode.props), dragNodesKeys: dragNodesKeys.slice(), dropPosition: dropPosition + Number(posArr[posArr.length - 1]), dropToGap: false }; if (dropPosition !== 0) { dropResult.dropToGap = true; } if (onDrop) { onDrop(dropResult); } dragNode = null; }; const cleanDragState = () => { if (dragging) { setDragging(false); } }; const onNodeClick = ( e: React.MouseEvent, treeNode: EventDataNode ) => { const { onClick } = props; if (onClick) { onClick(e, treeNode); } }; const onNodeDoubleClick = ( e: React.MouseEvent, treeNode: EventDataNode ) => { const { onDoubleClick } = props; if (onDoubleClick) { onDoubleClick(e, treeNode); } }; const onNodeSelect = ( e: React.MouseEvent, treeNode: EventDataNode ) => { let $selectedKeys = selectedKeys; const keyEntities = props.keyEntities; const { onSelect, multiple } = 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); setUncontrolledState({ selectedKeys }); if (onSelect) { onSelect(selectedKeys, { event: "select", selected: targetSelected, node: treeNode, selectedNodes, nativeEvent: e.nativeEvent }); } }; const onNodeLoad = (treeNode: EventDataNode) => new Promise(resolve => { ({ loadedKeys = [], loadingKeys = [] }): any => { // const [loadData, onLoad] = props; const loadData = props.loadData; const onLoad = props.onLoad; const { key } = treeNode; if ( !loadData || loadedKeys.indexOf(key) !== -1 || loadingKeys.indexOf(key) !== -1 ) { return {}; } const promise = loadData(treeNode); promise.then(() => { const newLoadedKeys = arrAdd(loadedKeys, key); const newLoadingKeys = arrDel(loadingKeys, key); if (onLoad) { onLoad(newLoadedKeys, { event: "load", node: treeNode }); } setUncontrolledState({ loadedKeys: newLoadedKeys }); setLoadingKeys(newLoadingKeys); resolve(); }); return { loadingKeys: arrAdd(loadingKeys, key) }; }; }); const onNodeExpand = ( e: React.MouseEvent, treeNode: EventDataNode ) => { let $expandedKeys = expandedKeys; const { onExpand, loadData } = props; const { key, expanded } = treeNode; const targetExpanded = !expanded; if (targetExpanded) { $expandedKeys = arrAdd($expandedKeys, key); } else { $expandedKeys = arrDel($expandedKeys, key); } const flattenNodes: FlattenNode[] = flattenTreeData(treeData, expandedKeys); setUncontrolledState({ expandedKeys, flattenNodes }, true); if (onExpand) { onExpand(expandedKeys, { node: treeNode, expanded: targetExpanded, nativeEvent: e.nativeEvent }); } //Async Load data if (targetExpanded && loadData) { const loadPromise = onNodeLoad(treeNode); return loadPromise ? loadPromise.then(() => { const newFlattenTreeData = flattenTreeData(treeData, expandedKeys); setUncontrolledState({ flattenNodes: newFlattenTreeData }); }) : null; } return null; }; const onNodeMouseEnter = ( event: React.MouseEvent, node: EventDataNode ) => { const { onMouseEnter } = props; if (onMouseEnter) { onMouseEnter({ event, node }); } }; const onNodeMouseLeave = ( event: React.MouseEvent, node: EventDataNode ) => { const { onMouseLeave } = props; if (onMouseLeave) { onMouseLeave({ event, node }); } }; const onNodeContextMenu = ( event: React.MouseEvent, node: EventDataNode ) => { const { onRightClick } = props; if (onRightClick) { event.preventDefault(); onRightClick({ event, node }); } }; const onFocus: React.FocusEventHandler = (...args) => { const { onFocus } = props; setFocused(true); if (onFocus) { onFocus(...args); } }; const onBlur: React.FocusEventHandler = (...args) => { const { onBlur } = props; setFocused(false); onActiveChange(null); if (onBlur) { onBlur(...args); } }; const getTreeNodeRequiredProps = () => { const keyEntities = props.keyEntities; return { expandedKeys: expandedKeys || [], selectedKey: selectedKeys || [], loadedKeys: loadedKeys || [], loadingKeys: loadingKeys || [], dragOverNodeKey, dropPosition, keyEntities }; }; const onActiveChange = (activeKey: Key) => { const { onActiveChange } = props; setActiveKey(activeKey); if (activeKey !== null) { scrollTo({ key: activeKey }); } if (onActiveChange) { onActiveChange(activeKey); } }; const getActiveItem = () => { if (activeKey === null) { return null; } return flattenNodes.find(({ data: { key } }) => key === activeKey) || null; }; // const offsetActiveKey = (offset: number) => { // 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; // onActiveChange(key); // } else { // onActiveChange(null); // } // }; //Only update the value which is not in props const setUncontrolledState = ( state: Partial, atomic: boolean = false ) => { let needSync = false; let allPassed = true; const [newState, setNewState] = React.useState<{}>({}); Object.keys(state).forEach(name => { if (name in props) { allPassed = false; return; } needSync = true; newState[name] = state[name]; }); if (needSync && (!atomic || allPassed)) { setNewState(newState); } }; const scrollTo: ScrollTo = scroll => { listRef.current.scrollTo(scroll); }; const { prefixCls, className, style, showLine, focusable, tabIndex = 0, selectable, showIcon, icon, switcherIcon, draggable, disabled, motion, loadData, filterTreeNode, height, itemHeight, keyEntities } = props; const domProps: React.HTMLAttributes = getDataAndAria(props); return (
); }; export { TreeProps }; export default Tree;