/** * ============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 { SvgIconProps } from '@material-ui/core/SvgIcon'; import { List, ListItem, TextField, ListItemText, ListItemIcon, WithTheme, withTheme, Omit } from '@material-ui/core'; 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'; export interface ITreeItem { disabled?: boolean; icon?: React.ComponentType; } type TreeViewComponentState = { /** All indices of all expanded Items */ expandedItems: TData[]; /** The index of the active iten or undefined if no item is active. */ activeItem: undefined | TData; /** The search term or undefined if search is corrently not active. */ searchTerm: undefined | string; } type TreeViewComponentBaseProps = WithTheme & { items: TData[]; contentProperty: keyof Omit; childrenProperty: keyof Omit; useFolderIcons?: boolean; enableSearchBar?: boolean; autoExpandFolder?: boolean; style?: React.CSSProperties; itemHeight?: number; depthOffset?: number; } type TreeViewComponentWithInternalStateProps = TreeViewComponentBaseProps & { onItemClick?: (item: TData) => void; onFolderClick?: (item: TData) => void; } type TreeViewComponentWithExternalStateProps = TreeViewComponentBaseProps & TreeViewComponentState & { onSearch: (searchTerm: string) => void; onItemClick: (item: TData) => void; onFolderClick: (item: TData) => 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; const styles = { root: { padding: 0, paddingBottom: 8, paddingTop: children ? 0 : 8, ...this.props.style }, search: { padding: `0px ${ this.props.theme.spacing.unit }px` } }; return (
{ children } { enableSearchBar && || null } { this.renderItems(items, searchTerm && searchTerm.toLowerCase()) }
); } private itemIndex: number = 0; private renderItems = (items: TData[], searchTerm: string | undefined, depth: number = 1) => { return items.reduce((acc, item) => { const children = this.props.childrenProperty && ((item as any)[this.props.childrenProperty] as TData[]); const childrenJsx = children && this.renderItems(children, searchTerm, depth + 1); const expanded = searchTerm ? children && childrenJsx.length > 0 : !children ? false : this.state.expandedItems.indexOf(item) > -1; const isFolder = children !== undefined; const itemJsx = this.renderItem(item, searchTerm, depth, isFolder, expanded); itemJsx && acc.push(itemJsx); if (isFolder && expanded) { acc.push(...childrenJsx); } return acc; }, [] as JSX.Element[]); } private renderItem = (item: TData, 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.unit * 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 as any)[this.props.contentProperty] as string || ''; // 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: TData) => { // 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: TData) => { // 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'>>; export const TreeView = withTheme()(TreeViewComponent); export default TreeView;