2 * ============LICENSE_START========================================================================
3 * ONAP : ccsdk feature sdnr wt odlux
4 * =================================================================================================
5 * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
6 * =================================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8 * in compliance with the License. You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software distributed under the License
13 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
14 * or implied. See the License for the specific language governing permissions and limitations under
16 * ============LICENSE_END==========================================================================
18 import * as React from 'react';
19 import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
21 import { List, ListItem, TextField, ListItemText, ListItemIcon, WithTheme, withTheme, Omit } from '@material-ui/core';
23 import { SvgIconProps } from '@material-ui/core/SvgIcon';
24 import FileIcon from '@material-ui/icons/InsertDriveFile';
25 import CloseIcon from '@material-ui/icons/ExpandLess';
26 import OpenIcon from '@material-ui/icons/ExpandMore';
27 import FolderIcon from '@material-ui/icons/Folder';
29 const styles = (theme: Theme) => createStyles({
36 padding: `0px ${theme.spacing(1)}px`
40 export type TreeItem<TData = { }> = {
42 icon?: React.ComponentType<SvgIconProps>;
45 contentClass?: string;
46 children?: TreeItem<TData>[];
50 type TreeViewComponentState<TData = { }> = {
51 /** All indices of all expanded Items */
52 expandedItems: TreeItem<TData>[];
53 /** The index of the active iten or undefined if no item is active. */
54 activeItem: undefined | TreeItem<TData>;
55 /** The search term or undefined if search is corrently not active. */
56 searchTerm: undefined | string;
59 type TreeViewComponentBaseProps<TData = {}> = WithTheme & WithStyles<typeof styles> & {
61 items: TreeItem<TData>[];
62 useFolderIcons?: boolean;
63 enableSearchBar?: boolean;
64 autoExpandFolder?: boolean;
65 style?: React.CSSProperties;
70 type TreeViewComponentWithInternalStateProps<TData = { }> = TreeViewComponentBaseProps<TData> & {
71 onItemClick?: (item: TreeItem<TData>) => void;
72 onFolderClick?: (item: TreeItem<TData>) => void;
75 type TreeViewComponentWithExternalStateProps<TData = { }> = TreeViewComponentBaseProps<TData> & TreeViewComponentState<TData> & {
76 onSearch: (searchTerm: string) => void;
77 onItemClick: (item: TreeItem<TData>) => void;
78 onFolderClick: (item: TreeItem<TData>) => void;
81 type TreeViewComponentProps<TData = { }> =
82 | TreeViewComponentWithInternalStateProps<TData>
83 | TreeViewComponentWithExternalStateProps<TData>;
85 function isTreeViewComponentWithExternalStateProps(props: TreeViewComponentProps): props is TreeViewComponentWithExternalStateProps {
86 const propsWithExternalState = (props as TreeViewComponentWithExternalStateProps)
88 propsWithExternalState.onSearch instanceof Function ||
89 propsWithExternalState.expandedItems !== undefined ||
90 propsWithExternalState.activeItem !== undefined ||
91 propsWithExternalState.searchTerm !== undefined
95 class TreeViewComponent<TData = { }> extends React.Component<TreeViewComponentProps<TData>, TreeViewComponentState<TData>> {
98 * Initializes a new instance.
100 constructor(props: TreeViewComponentProps<TData>) {
105 activeItem: undefined,
106 searchTerm: undefined
110 render(): JSX.Element {
112 const { searchTerm } = this.state;
113 const { children, items, enableSearchBar } = this.props;
116 <div className={this.props.className ? `${this.props.classes.root} ${this.props.className}` : this.props.classes.root} style={this.props.style}>
118 {enableSearchBar && <TextField label={"Search"} fullWidth={true} className={ this.props.classes.search } value={searchTerm} onChange={this.onChangeSearchText} /> || null}
120 {this.renderItems(items, searchTerm && searchTerm.toLowerCase())}
126 private itemIndex: number = 0;
127 private renderItems = (items: TreeItem<TData>[], searchTerm: string | undefined, depth: number = 1) => {
128 return items.reduce((acc, item) => {
130 const children = item.children; // this.props.childrenProperty && ((item as any)[this.props.childrenProperty] as TData[]);
131 const childrenJsx = children && this.renderItems(children, searchTerm, depth + 1);
133 const expanded = searchTerm
134 ? childrenJsx && childrenJsx.length > 0
137 : this.state.expandedItems.indexOf(item) > -1;
138 const isFolder = children !== undefined;
140 const itemJsx = this.renderItem(item, searchTerm, depth, isFolder, expanded || false);
141 itemJsx && acc.push(itemJsx);
143 if (isFolder && expanded && childrenJsx) {
144 acc.push(...childrenJsx);
148 }, [] as JSX.Element[]);
150 private renderItem = (item: TreeItem<TData>, searchTerm: string | undefined, depth: number, isFolder: boolean, expanded: boolean): JSX.Element | null => {
153 paddingLeft: (((this.props.depthOffset || 0) + depth) * this.props.theme.spacing(3)),
154 backgroundColor: this.state.activeItem === item ? this.props.theme.palette.action.selected : undefined,
155 height: this.props.itemHeight || undefined,
156 cursor: item.disabled ? 'not-allowed' : 'pointer',
157 color: item.disabled ? this.props.theme.palette.text.disabled : this.props.theme.palette.text.primary,
159 transform: 'translateZ(0)',
163 const text = item.content || ''; // need to keep track of search
164 const matchIndex = searchTerm ? text.toLowerCase().indexOf(searchTerm) : -1;
165 const searchTermLength = searchTerm && searchTerm.length || 0;
167 const handleClickCreator = (isIcon: boolean) => (event: React.SyntheticEvent) => {
168 if (item.disabled) return;
169 event.preventDefault();
170 event.stopPropagation();
171 if (isFolder && (this.props.autoExpandFolder || isIcon)) {
172 this.props.onFolderClick ? this.props.onFolderClick(item) : this.onFolderClick(item);
174 this.props.onItemClick ? this.props.onItemClick(item) : this.onItemClick(item);
178 return ((searchTerm && (matchIndex > -1 || expanded) || !searchTerm)
180 <ListItem key={`tree-list-${this.itemIndex++}`} style={styles.item} onClick={handleClickCreator(false)} button >
182 { // display the left icon
183 (this.props.useFolderIcons && <ListItemIcon>{isFolder ? <FolderIcon /> : <FileIcon />}</ListItemIcon>) ||
184 (item.icon && (<ListItemIcon className={ item.iconClass }><item.icon /></ListItemIcon>))}
187 { // highlight search result
190 {text.substring(0, matchIndex)}
193 display: 'inline-block',
194 backgroundColor: 'rgba(255,235,59,0.5)',
198 {text.substring(matchIndex, matchIndex + searchTermLength)}
200 {text.substring(matchIndex + searchTermLength)}
202 : (<ListItemText className={ item.contentClass } primary={text} />)
205 { // display the right icon, depending on the state
206 !isFolder ? null : expanded ? (<OpenIcon onClick={handleClickCreator(true)} />) : (<CloseIcon onClick={handleClickCreator(true)} />)}
213 private onFolderClick = (item: TreeItem<TData>) => {
214 // toggle items with children
215 if (this.state.searchTerm) return;
216 const indexOfItemToToggle = this.state.expandedItems.indexOf(item);
217 if (indexOfItemToToggle === -1) {
219 expandedItems: [...this.state.expandedItems, item],
224 ...this.state.expandedItems.slice(0, indexOfItemToToggle),
225 ...this.state.expandedItems.slice(indexOfItemToToggle + 1),
231 private onItemClick = (item: TreeItem<TData>) => {
232 // activate items without children
238 private onChangeSearchText = (event: React.ChangeEvent<HTMLInputElement>) => {
239 event.preventDefault();
240 event.stopPropagation();
242 if (isTreeViewComponentWithExternalStateProps(this.props)) {
243 this.props.onSearch(event.target.value)
246 searchTerm: event.target.value
251 static getDerivedStateFromProps(props: TreeViewComponentProps, state: TreeViewComponentState): TreeViewComponentState {
252 if (isTreeViewComponentWithExternalStateProps(props)) {
255 expandedItems: props.expandedItems || [],
256 activeItem: props.activeItem,
257 searchTerm: props.searchTerm
263 public static defaultProps = {
264 useFolderIcons: false,
265 enableSearchBar: false,
266 autoExpandFolder: false,
271 export type TreeViewCtorType<TData = { }> = new () => React.Component<Omit<TreeViewComponentProps<TData>, 'theme'|'classes'>>;
273 export const TreeView = withTheme(withStyles(styles)(TreeViewComponent));
274 export default TreeView;