update odlux stage 3
[ccsdk/features.git] / sdnr / wt / odlux / framework / src / components / material-ui / treeView.tsx
1 /**
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
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
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
15  * the License.
16  * ============LICENSE_END==========================================================================
17  */
18 import * as React from 'react';
19 import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
20
21 import { List, ListItem, TextField, ListItemText, ListItemIcon, WithTheme, withTheme, Omit } from '@material-ui/core';
22
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';
28
29 const styles = (theme: Theme) => createStyles({
30   root: {
31     padding: 0,
32     paddingBottom: 8,
33     paddingTop: 8,
34   },
35   search: {
36     padding: `0px ${theme.spacing(1)}px`
37   }
38 });
39
40 export type TreeItem<TData = { }> = {
41   disabled?: boolean;
42   icon?: React.ComponentType<SvgIconProps>;
43   iconClass?: string;
44   content: string;
45   contentClass?: string;
46   children?: TreeItem<TData>[];
47   value?: TData;
48 }
49
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;
57 }
58
59 type TreeViewComponentBaseProps<TData = {}> = WithTheme & WithStyles<typeof styles> & {
60   className?: string;
61   items: TreeItem<TData>[];
62   useFolderIcons?: boolean;
63   enableSearchBar?: boolean;
64   autoExpandFolder?: boolean;
65   style?: React.CSSProperties;
66   itemHeight?: number;
67   depthOffset?: number;
68 }
69
70 type TreeViewComponentWithInternalStateProps<TData = { }> = TreeViewComponentBaseProps<TData> & {
71   onItemClick?: (item: TreeItem<TData>) => void;
72   onFolderClick?: (item: TreeItem<TData>) => void;
73 }
74
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;
79 }
80
81 type TreeViewComponentProps<TData = { }> =
82   | TreeViewComponentWithInternalStateProps<TData>
83   | TreeViewComponentWithExternalStateProps<TData>;
84
85 function isTreeViewComponentWithExternalStateProps(props: TreeViewComponentProps): props is TreeViewComponentWithExternalStateProps {
86   const propsWithExternalState = (props as TreeViewComponentWithExternalStateProps)
87   return (
88     propsWithExternalState.onSearch instanceof Function ||
89     propsWithExternalState.expandedItems !== undefined ||
90     propsWithExternalState.activeItem !== undefined ||
91     propsWithExternalState.searchTerm !== undefined
92   );
93 }
94
95 class TreeViewComponent<TData = { }> extends React.Component<TreeViewComponentProps<TData>, TreeViewComponentState<TData>> {
96
97   /**
98     * Initializes a new instance.
99     */
100   constructor(props: TreeViewComponentProps<TData>) {
101     super(props);
102
103     this.state = {
104       expandedItems: [],
105       activeItem: undefined,
106       searchTerm: undefined
107     };
108   }
109
110   render(): JSX.Element {
111     this.itemIndex = 0;
112     const { searchTerm } = this.state;
113     const { children, items, enableSearchBar } = this.props;
114
115     return (
116       <div className={this.props.className ? `${this.props.classes.root} ${this.props.className}` : this.props.classes.root} style={this.props.style}>
117         {children}
118         {enableSearchBar && <TextField label={"Search"} fullWidth={true} className={ this.props.classes.search } value={searchTerm} onChange={this.onChangeSearchText} /> || null}
119         <List>
120           {this.renderItems(items, searchTerm && searchTerm.toLowerCase())}
121         </List>
122       </div>
123     );
124   }
125
126   private itemIndex: number = 0;
127   private renderItems = (items: TreeItem<TData>[], searchTerm: string | undefined, depth: number = 1) => {
128     return items.reduce((acc, item) => {
129
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);
132
133       const expanded = searchTerm
134         ? childrenJsx && childrenJsx.length > 0
135         : !children
136           ? false
137           : this.state.expandedItems.indexOf(item) > -1;
138       const isFolder = children !== undefined;
139
140       const itemJsx = this.renderItem(item, searchTerm, depth, isFolder, expanded || false);
141       itemJsx && acc.push(itemJsx);
142
143       if (isFolder && expanded && childrenJsx) {
144         acc.push(...childrenJsx);
145       }
146       return acc;
147
148     }, [] as JSX.Element[]);
149   }
150   private renderItem = (item: TreeItem<TData>, searchTerm: string | undefined, depth: number, isFolder: boolean, expanded: boolean): JSX.Element | null => {
151     const styles = {
152       item: {
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,
158         overflow: 'hidden',
159         transform: 'translateZ(0)',
160       }
161     };
162
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;
166
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);
173       } else {
174         this.props.onItemClick ? this.props.onItemClick(item) : this.onItemClick(item);
175       }
176     };
177
178     return ((searchTerm && (matchIndex > -1 || expanded) || !searchTerm)
179       ? (
180         <ListItem key={`tree-list-${this.itemIndex++}`} style={styles.item} onClick={handleClickCreator(false)} button >
181
182           { // display the left icon
183             (this.props.useFolderIcons && <ListItemIcon>{isFolder ? <FolderIcon /> : <FileIcon />}</ListItemIcon>) ||
184             (item.icon && (<ListItemIcon className={ item.iconClass }><item.icon /></ListItemIcon>))}
185
186
187           { // highlight search result
188             matchIndex > -1
189               ? (<span>
190                 {text.substring(0, matchIndex)}
191                 <span
192                   style={{
193                     display: 'inline-block',
194                     backgroundColor: 'rgba(255,235,59,0.5)',
195                     padding: '3px',
196                   }}
197                 >
198                   {text.substring(matchIndex, matchIndex + searchTermLength)}
199                 </span>
200                 {text.substring(matchIndex + searchTermLength)}
201               </span>)
202               : (<ListItemText className={ item.contentClass } primary={text} />)
203           }
204
205           { // display the right icon, depending on the state
206             !isFolder ? null : expanded ? (<OpenIcon onClick={handleClickCreator(true)} />) : (<CloseIcon onClick={handleClickCreator(true)} />)}
207         </ListItem>
208       )
209       : null
210     );
211   }
212
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) {
218       this.setState({
219         expandedItems: [...this.state.expandedItems, item],
220       });
221     } else {
222       this.setState({
223         expandedItems: [
224           ...this.state.expandedItems.slice(0, indexOfItemToToggle),
225           ...this.state.expandedItems.slice(indexOfItemToToggle + 1),
226         ]
227       });
228     }
229   };
230
231   private onItemClick = (item: TreeItem<TData>) => {
232     // activate items without children
233     this.setState({
234       activeItem: item,
235     });
236   };
237
238   private onChangeSearchText = (event: React.ChangeEvent<HTMLInputElement>) => {
239     event.preventDefault();
240     event.stopPropagation();
241
242     if (isTreeViewComponentWithExternalStateProps(this.props)) {
243       this.props.onSearch(event.target.value)
244     } else {
245       this.setState({
246         searchTerm: event.target.value
247       });
248     }
249   };
250
251   static getDerivedStateFromProps(props: TreeViewComponentProps, state: TreeViewComponentState): TreeViewComponentState {
252     if (isTreeViewComponentWithExternalStateProps(props)) {
253       return {
254         ...state,
255         expandedItems: props.expandedItems || [],
256         activeItem: props.activeItem,
257         searchTerm: props.searchTerm
258       };
259     }
260     return state;
261   }
262
263   public static defaultProps = {
264     useFolderIcons: false,
265     enableSearchBar: false,
266     autoExpandFolder: false,
267     depthOffset: 0
268   }
269 }
270
271 export type TreeViewCtorType<TData = { }> = new () => React.Component<Omit<TreeViewComponentProps<TData>, 'theme'|'classes'>>;
272
273 export const TreeView = withTheme(withStyles(styles)(TreeViewComponent));
274 export default TreeView;