import * as React from "react"; import classNames from "classnames"; // @ts-ignore import { TreeContext, TreeContextProps } from "./context-types"; import { getDataAndAria } from "./utils/common-util"; import { IconType, Key, DataNode } from "./interface"; import { Indent } from "./indent"; import { convertNodePropsToEventData } from "./utils/tree-util"; const ICON_OPEN = "open"; const ICON_CLOSE = "close"; const defaultTitle = "---"; interface TreeNodeProps { eventKey?: Key; // Pass by parent `cloneElement` prefixCls?: string; className?: string; style?: React.CSSProperties; // By parent expanded?: boolean; selected?: boolean; loaded?: boolean; loading?: boolean; title?: React.ReactNode | ((data: DataNode) => React.ReactNode); dragOver?: boolean; dragOverGapTop?: boolean; dragOverGapBottom?: boolean; pos?: string; domRef?: React.Ref; /** New added in Tree for easy data access */ data?: DataNode; isStart?: boolean[]; isEnd?: boolean[]; active: boolean; onMouseMove?: React.MouseEventHandler; // By user isLeaf?: boolean; selectable?: boolean; disabled?: boolean; icon?: IconType; switcherIcon?: IconType; children?: React.ReactNode; context?: TreeContextProps; } interface TreeNodeState { dragNodeHighlight: boolean; } class TreeNode extends React.Component { public state = { dragNodeHighlight: false }; public selectHandle: HTMLSpanElement; // Isomorphic needn't load data in server side componentDidMount() { this.syncLoadData(this.props); } componentDidUpdate() { this.syncLoadData(this.props); } onSelectorClick = e => { // Click trigger before select operation const { context: { onNodeClick }, selected } = this.props; onNodeClick(e, convertNodePropsToEventData(this.props)); if (this.isSelectable() && !selected) { this.onSelect(e); } else { return; } }; onSelectorDoubleClick = e => { const { context: { onNodeDoubleClick } } = this.props; onNodeDoubleClick(e, convertNodePropsToEventData(this.props)); }; onSelect = e => { if (this.isDisabled()) return; const { context: { onNodeSelect } } = this.props; e.preventDefault(); onNodeSelect(e, convertNodePropsToEventData(this.props)); }; onMouseEnter = e => { const { context: { onNodeMouseEnter } } = this.props; onNodeMouseEnter(e, convertNodePropsToEventData(this.props)); }; onMouseLeave = e => { const { context: { onNodeMouseLeave } } = this.props; onNodeMouseLeave(e, convertNodePropsToEventData(this.props)); }; onContextMenu = e => { const { context: { onNodeContextMenu } } = this.props; onNodeContextMenu(e, convertNodePropsToEventData(this.props)); }; onDragStart = e => { const { context: { onNodeDragStart } } = this.props; e.stopPropagation(); this.setState({ dragNodeHighlight: false }); onNodeDragStart(e, this); try { // ie throw error // firefox-need-it e.dataTransfer.setData("text/plain", ""); } catch (error) { // empty } }; onDragEnter = e => { const { context: { onNodeDragEnter } } = this.props; e.preventDefault(); e.stopPropagation(); onNodeDragEnter(e, this); }; onDragOver = e => { const { context: { onNodeDragOver } } = this.props; e.preventDefault(); e.stopPropagation(); onNodeDragOver(e, this); }; onDragLeave = e => { const { context: { onNodeDragLeave } } = this.props; e.stopPropagation(); onNodeDragLeave(e, this); }; onDragEnd = e => { const { context: { onNodeDragEnd } } = this.props; e.stopPropagation(); this.setState({ dragNodeHighlight: false }); onNodeDragEnd(e, this); }; onDrop = e => { const { context: { onNodeDrop } } = this.props; e.preventDefault(); e.stopPropagation(); this.setState({ dragNodeHighlight: false }); onNodeDrop(e, this); }; // Disabled item still can be switch onExpand: React.MouseEventHandler = e => { const { context: { onNodeExpand } } = this.props; onNodeExpand(e, convertNodePropsToEventData(this.props)); }; // Drag usage setSelectHandle = node => { this.selectHandle = node; }; getNodeState = () => { const { expanded } = this.props; if (this.isChild()) { return null; } return expanded ? ICON_OPEN : ICON_CLOSE; }; hasChildren = () => { const { eventKey } = this.props; const { context: { keyEntities } } = this.props; const { children } = keyEntities[eventKey] || {}; return !!(children || []).length; }; isChild = () => { const { isLeaf, loaded } = this.props; const { context: { loadData } } = this.props; const hasChildren = this.hasChildren(); if (isLeaf === false) { return false; } return ( isLeaf || (!loadData && !hasChildren) || (loadData && loaded && !hasChildren) ); }; isDisabled = () => { const { disabled } = this.props; const { context: { disabled: treeDisabled } } = this.props; return !!(treeDisabled || disabled); }; // Load data to avoid default expanded tree without data syncLoadData = props => { const { expanded, loading, loaded } = props; const { context: { loadData, onNodeLoad } } = this.props; if (loading) return; // read from state to avoid loadData at same time if (loadData && expanded && !this.isChild()) { // We needn't reload data when has children in sync logic // It's only needed in node expanded if (!this.hasChildren() && !loaded) { onNodeLoad(convertNodePropsToEventData(this.props)); } } }; isSelectable() { const { selectable } = this.props; const { context: { selectable: treeSelectable } } = this.props; // Ignore when selectable is undefined or null if (typeof selectable === "boolean") { return selectable; } return treeSelectable; } // Switcher renderSwitcher = () => { const { expanded, switcherIcon: switcherIconFromProps } = this.props; const { context: { prefixCls, switcherIcon: switcherIconFromCtx } } = this.props; const switcherIcon = switcherIconFromProps || switcherIconFromCtx; if (this.isChild()) { return (
{typeof switcherIcon === "function" ? switcherIcon({ ...this.props, isLeaf: true }) : switcherIcon}
); } const switcherCls = classNames( `${prefixCls}-switcher`, `${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}` ); if (switcherIcon) { return (
{typeof switcherIcon === "function" ? switcherIcon({ ...this.props, isLeaf: false }) : switcherIcon}
); } else { return ( //icon
👉 {typeof switcherIcon === "function" ? switcherIcon({ ...this.props, isLeaf: false }) : switcherIcon}
); } }; renderIcon = () => { const { loading } = this.props; const { context: { prefixCls } } = this.props; if (this.getNodeState()) { return 📕; } if (loading) { return ; } return 📗; }; // Icon + Title renderSelector = () => { const { dragNodeHighlight } = this.state; const { title, selected, icon, loading, data } = this.props; const { context: { prefixCls, showIcon, icon: treeIcon, draggable, loadData } } = this.props; const disabled = this.isDisabled(); const wrapClass = `${prefixCls}-node-content-wrapper`; // Icon - Still show loading icon when loading without showIcon let $icon; if (showIcon) { const currentIcon = icon || treeIcon; $icon = currentIcon ? ( {typeof currentIcon === "function" ? currentIcon(this.props) : currentIcon} ) : ( this.renderIcon() ); } else if (loadData && loading) { $icon = this.renderIcon(); } // Title const $title = ( {typeof title === "function" ? title(data) : title} ); return ( {$icon} {$title} ); }; render() { const { eventKey, className, style, dragOver, dragOverGapTop, dragOverGapBottom, isLeaf, isStart, isEnd, expanded, selected, loading, domRef, active, onMouseMove, ...otherProps } = this.props; const { context: { prefixCls, filterTreeNode, draggable, keyEntities } } = this.props; const disabled = this.isDisabled(); const dataOrAriaAttributeProps = getDataAndAria(otherProps); const { level } = keyEntities[eventKey] || {}; return (
{this.renderSwitcher()} {this.renderSelector()}
); } } const ContextTreeNode: React.FC | any = props => { const context = React.useContext(TreeContext); return ; }; ContextTreeNode.displayName = "TreeNode"; ContextTreeNode.defaultProps = { title: defaultTitle }; (ContextTreeNode as any).isTreeNode = 1; export { TreeNodeProps, TreeNode, TreeNodeState }; export default ContextTreeNode;