/** * ============LICENSE_START======================================================================== * ONAP : ccsdk feature sdnr wt odlux * ================================================================================================= * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. * ================================================================================================= * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. * ============LICENSE_END========================================================================== */ import * as React from 'react'; import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; import { List, ListItem, TextField, ListItemText, ListItemIcon, WithTheme, withTheme, Omit } from '@material-ui/core'; import { SvgIconProps } from '@material-ui/core/SvgIcon'; import FileIcon from '@material-ui/icons/InsertDriveFile'; import CloseIcon from '@material-ui/icons/ExpandLess'; import OpenIcon from '@material-ui/icons/ExpandMore'; import FolderIcon from '@material-ui/icons/Folder'; const styles = (theme: Theme) => createStyles({ root: { padding: 0, paddingBottom: 8, paddingTop: 8, }, search: { padding: `0px ${theme.spacing(1)}px` } }); export type TreeItem = { disabled?: boolean; icon?: React.ComponentType; iconClass?: string; content: string; contentClass?: string; children?: TreeItem[]; value?: TData; } type TreeViewComponentState = { /** All indices of all expanded Items */ expandedItems: TreeItem[]; /** The index of the active iten or undefined if no item is active. */ activeItem: undefined | TreeItem; /** The search term or undefined if search is corrently not active. */ searchTerm: undefined | string; } type TreeViewComponentBaseProps = WithTheme & WithStyles & { className?: string; items: TreeItem[]; useFolderIcons?: boolean; enableSearchBar?: boolean; autoExpandFolder?: boolean; style?: React.CSSProperties; itemHeight?: number; depthOffset?: number; } type TreeViewComponentWithInternalStateProps = TreeViewComponentBaseProps & { onItemClick?: (item: TreeItem) => void; onFolderClick?: (item: TreeItem) => void; } type TreeViewComponentWithExternalStateProps = TreeViewComponentBaseProps & TreeViewComponentState & { onSearch: (searchTerm: string) => void; onItemClick: (item: TreeItem) => void; onFolderClick: (item: TreeItem) => void; } type TreeViewComponentProps = | TreeViewComponentWithInternalStateProps | TreeViewComponentWithExternalStateProps; function isTreeViewComponentWithExternalStateProps(props: TreeViewComponentProps): props is TreeViewComponentWithExternalStateProps { const propsWithExternalState = (props as TreeViewComponentWithExternalStateProps) return ( propsWithExternalState.onSearch instanceof Function || propsWithExternalState.expandedItems !== undefined || propsWithExternalState.activeItem !== undefined || propsWithExternalState.searchTerm !== undefined ); } class TreeViewComponent extends React.Component, TreeViewComponentState> { /** * Initializes a new instance. */ constructor(props: TreeViewComponentProps) { super(props); this.state = { expandedItems: [], activeItem: undefined, searchTerm: undefined }; } render(): JSX.Element { this.itemIndex = 0; const { searchTerm } = this.state; const { children, items, enableSearchBar } = this.props; return (
{children} {enableSearchBar && || null} {this.renderItems(items, searchTerm && searchTerm.toLowerCase())}
); } private itemIndex: number = 0; private renderItems = (items: TreeItem[], searchTerm: string | undefined, depth: number = 1) => { return items.reduce((acc, item) => { const children = item.children; // this.props.childrenProperty && ((item as any)[this.props.childrenProperty] as TData[]); const childrenJsx = children && this.renderItems(children, searchTerm, depth + 1); const expanded = searchTerm ? childrenJsx && childrenJsx.length > 0 : !children ? false : this.state.expandedItems.indexOf(item) > -1; const isFolder = children !== undefined; const itemJsx = this.renderItem(item, searchTerm, depth, isFolder, expanded || false); itemJsx && acc.push(itemJsx); if (isFolder && expanded && childrenJsx) { acc.push(...childrenJsx); } return acc; }, [] as JSX.Element[]); } private renderItem = (item: TreeItem, searchTerm: string | undefined, depth: number, isFolder: boolean, expanded: boolean): JSX.Element | null => { const styles = { item: { paddingLeft: (((this.props.depthOffset || 0) + depth) * this.props.theme.spacing(3)), backgroundColor: this.state.activeItem === item ? this.props.theme.palette.action.selected : undefined, height: this.props.itemHeight || undefined, cursor: item.disabled ? 'not-allowed' : 'pointer', color: item.disabled ? this.props.theme.palette.text.disabled : this.props.theme.palette.text.primary, overflow: 'hidden', transform: 'translateZ(0)', } }; const text = item.content || ''; // need to keep track of search const matchIndex = searchTerm ? text.toLowerCase().indexOf(searchTerm) : -1; const searchTermLength = searchTerm && searchTerm.length || 0; const handleClickCreator = (isIcon: boolean) => (event: React.SyntheticEvent) => { if (item.disabled) return; event.preventDefault(); event.stopPropagation(); if (isFolder && (this.props.autoExpandFolder || isIcon)) { this.props.onFolderClick ? this.props.onFolderClick(item) : this.onFolderClick(item); } else { this.props.onItemClick ? this.props.onItemClick(item) : this.onItemClick(item); } }; return ((searchTerm && (matchIndex > -1 || expanded) || !searchTerm) ? ( { // display the left icon (this.props.useFolderIcons && {isFolder ? : }) || (item.icon && ())} { // highlight search result matchIndex > -1 ? ( {text.substring(0, matchIndex)} {text.substring(matchIndex, matchIndex + searchTermLength)} {text.substring(matchIndex + searchTermLength)} ) : () } { // display the right icon, depending on the state !isFolder ? null : expanded ? () : ()} ) : null ); } private onFolderClick = (item: TreeItem) => { // toggle items with children if (this.state.searchTerm) return; const indexOfItemToToggle = this.state.expandedItems.indexOf(item); if (indexOfItemToToggle === -1) { this.setState({ expandedItems: [...this.state.expandedItems, item], }); } else { this.setState({ expandedItems: [ ...this.state.expandedItems.slice(0, indexOfItemToToggle), ...this.state.expandedItems.slice(indexOfItemToToggle + 1), ] }); } }; private onItemClick = (item: TreeItem) => { // activate items without children this.setState({ activeItem: item, }); }; private onChangeSearchText = (event: React.ChangeEvent) => { event.preventDefault(); event.stopPropagation(); if (isTreeViewComponentWithExternalStateProps(this.props)) { this.props.onSearch(event.target.value) } else { this.setState({ searchTerm: event.target.value }); } }; static getDerivedStateFromProps(props: TreeViewComponentProps, state: TreeViewComponentState): TreeViewComponentState { if (isTreeViewComponentWithExternalStateProps(props)) { return { ...state, expandedItems: props.expandedItems || [], activeItem: props.activeItem, searchTerm: props.searchTerm }; } return state; } public static defaultProps = { useFolderIcons: false, enableSearchBar: false, autoExpandFolder: false, depthOffset: 0 } } export type TreeViewCtorType = new () => React.Component, 'theme'|'classes'>>; export const TreeView = withTheme(withStyles(styles)(TreeViewComponent)); export default TreeView;