1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import classNames from "classnames";
|
18 | import * as React from "react";
|
19 |
|
20 | import * as Classes from "../../common/classes";
|
21 | import { DISPLAYNAME_PREFIX, Props, MaybeElement } from "../../common/props";
|
22 | import { Collapse } from "../collapse/collapse";
|
23 | import { Icon, IconName } from "../icon/icon";
|
24 |
|
25 |
|
26 | export type TreeNodeInfo<T = {}> = ITreeNode<T>;
|
27 |
|
28 |
|
29 | export interface ITreeNode<T = {}> extends Props {
|
30 | |
31 |
|
32 |
|
33 | childNodes?: Array<TreeNodeInfo<T>>;
|
34 |
|
35 | |
36 |
|
37 |
|
38 |
|
39 | disabled?: boolean;
|
40 |
|
41 | |
42 |
|
43 |
|
44 |
|
45 | hasCaret?: boolean;
|
46 |
|
47 | |
48 |
|
49 |
|
50 | icon?: IconName | MaybeElement;
|
51 |
|
52 | |
53 |
|
54 |
|
55 | id: string | number;
|
56 |
|
57 | |
58 |
|
59 | isExpanded?: boolean;
|
60 |
|
61 | |
62 |
|
63 |
|
64 |
|
65 |
|
66 | isSelected?: boolean;
|
67 |
|
68 | |
69 |
|
70 |
|
71 | label: string | JSX.Element;
|
72 |
|
73 | |
74 |
|
75 |
|
76 | secondaryLabel?: string | MaybeElement;
|
77 |
|
78 | |
79 |
|
80 |
|
81 |
|
82 |
|
83 | nodeData?: T;
|
84 | }
|
85 |
|
86 |
|
87 | export type TreeNodeProps<T = {}> = ITreeNodeProps<T>;
|
88 |
|
89 | export interface ITreeNodeProps<T = {}> extends TreeNodeInfo<T> {
|
90 | children?: React.ReactNode;
|
91 | contentRef?: (node: TreeNode<T>, element: HTMLDivElement | null) => void;
|
92 | depth: number;
|
93 | key?: string | number;
|
94 | onClick?: (node: TreeNode<T>, e: React.MouseEvent<HTMLDivElement>) => void;
|
95 | onCollapse?: (node: TreeNode<T>, e: React.MouseEvent<HTMLSpanElement>) => void;
|
96 | onContextMenu?: (node: TreeNode<T>, e: React.MouseEvent<HTMLDivElement>) => void;
|
97 | onDoubleClick?: (node: TreeNode<T>, e: React.MouseEvent<HTMLDivElement>) => void;
|
98 | onExpand?: (node: TreeNode<T>, e: React.MouseEvent<HTMLSpanElement>) => void;
|
99 | onMouseEnter?: (node: TreeNode<T>, e: React.MouseEvent<HTMLDivElement>) => void;
|
100 | onMouseLeave?: (node: TreeNode<T>, e: React.MouseEvent<HTMLDivElement>) => void;
|
101 | path: number[];
|
102 | }
|
103 |
|
104 |
|
105 | export class TreeNode<T = {}> extends React.Component<ITreeNodeProps<T>> {
|
106 | public static displayName = `${DISPLAYNAME_PREFIX}.TreeNode`;
|
107 |
|
108 | public static ofType<U>() {
|
109 | return TreeNode as new (props: ITreeNodeProps<U>) => TreeNode<U>;
|
110 | }
|
111 |
|
112 | public render() {
|
113 | const { children, className, disabled, icon, isExpanded, isSelected, label } = this.props;
|
114 | const classes = classNames(
|
115 | Classes.TREE_NODE,
|
116 | {
|
117 | [Classes.DISABLED]: disabled,
|
118 | [Classes.TREE_NODE_SELECTED]: isSelected,
|
119 | [Classes.TREE_NODE_EXPANDED]: isExpanded,
|
120 | },
|
121 | className,
|
122 | );
|
123 |
|
124 | const contentClasses = classNames(
|
125 | Classes.TREE_NODE_CONTENT,
|
126 | `${Classes.TREE_NODE_CONTENT}-${this.props.depth}`,
|
127 | );
|
128 |
|
129 | const eventHandlers =
|
130 | disabled === true
|
131 | ? {}
|
132 | : {
|
133 | onClick: this.handleClick,
|
134 | onContextMenu: this.handleContextMenu,
|
135 | onDoubleClick: this.handleDoubleClick,
|
136 | onMouseEnter: this.handleMouseEnter,
|
137 | onMouseLeave: this.handleMouseLeave,
|
138 | };
|
139 |
|
140 | return (
|
141 | <li className={classes}>
|
142 | <div className={contentClasses} ref={this.handleContentRef} {...eventHandlers}>
|
143 | {this.maybeRenderCaret()}
|
144 | <Icon className={Classes.TREE_NODE_ICON} icon={icon} />
|
145 | <span className={Classes.TREE_NODE_LABEL}>{label}</span>
|
146 | {this.maybeRenderSecondaryLabel()}
|
147 | </div>
|
148 | <Collapse isOpen={isExpanded}>{children}</Collapse>
|
149 | </li>
|
150 | );
|
151 | }
|
152 |
|
153 | private maybeRenderCaret() {
|
154 | const { children, isExpanded, disabled, hasCaret = React.Children.count(children) > 0 } = this.props;
|
155 | if (hasCaret) {
|
156 | const caretClasses = classNames(
|
157 | Classes.TREE_NODE_CARET,
|
158 | isExpanded ? Classes.TREE_NODE_CARET_OPEN : Classes.TREE_NODE_CARET_CLOSED,
|
159 | );
|
160 | const onClick = disabled === true ? undefined : this.handleCaretClick;
|
161 | return (
|
162 | <Icon
|
163 | title={isExpanded ? "Collapse group" : "Expand group"}
|
164 | className={caretClasses}
|
165 | onClick={onClick}
|
166 | icon={"chevron-right"}
|
167 | />
|
168 | );
|
169 | }
|
170 | return <span className={Classes.TREE_NODE_CARET_NONE} />;
|
171 | }
|
172 |
|
173 | private maybeRenderSecondaryLabel() {
|
174 | if (this.props.secondaryLabel != null) {
|
175 | return <span className={Classes.TREE_NODE_SECONDARY_LABEL}>{this.props.secondaryLabel}</span>;
|
176 | } else {
|
177 | return undefined;
|
178 | }
|
179 | }
|
180 |
|
181 | private handleCaretClick = (e: React.MouseEvent<HTMLElement>) => {
|
182 | e.stopPropagation();
|
183 | const { isExpanded, onCollapse, onExpand } = this.props;
|
184 | (isExpanded ? onCollapse : onExpand)?.(this, e);
|
185 | };
|
186 |
|
187 | private handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
188 | this.props.onClick?.(this, e);
|
189 | };
|
190 |
|
191 | private handleContentRef = (element: HTMLDivElement | null) => {
|
192 | this.props.contentRef?.(this, element);
|
193 | };
|
194 |
|
195 | private handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
|
196 | this.props.onContextMenu?.(this, e);
|
197 | };
|
198 |
|
199 | private handleDoubleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
200 | this.props.onDoubleClick?.(this, e);
|
201 | };
|
202 |
|
203 | private handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
|
204 | this.props.onMouseEnter?.(this, e);
|
205 | };
|
206 |
|
207 | private handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
|
208 | this.props.onMouseLeave?.(this, e);
|
209 | };
|
210 | }
|