aboutsummaryrefslogtreecommitdiffstats
path: root/sdnr/wt/odlux/framework/src/components/material-ui
diff options
context:
space:
mode:
Diffstat (limited to 'sdnr/wt/odlux/framework/src/components/material-ui')
-rw-r--r--sdnr/wt/odlux/framework/src/components/material-ui/index.ts3
-rw-r--r--sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx50
-rw-r--r--sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx56
-rw-r--r--sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx57
-rw-r--r--sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx159
-rw-r--r--sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx19
-rw-r--r--sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx251
7 files changed, 595 insertions, 0 deletions
diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/index.ts b/sdnr/wt/odlux/framework/src/components/material-ui/index.ts
new file mode 100644
index 000000000..890312ce2
--- /dev/null
+++ b/sdnr/wt/odlux/framework/src/components/material-ui/index.ts
@@ -0,0 +1,3 @@
+export { ListItemLink } from './listItemLink';
+export { Panel } from './panel';
+export { ToggleButton, ToggleButtonClassKey } from './toggleButton';
diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx
new file mode 100644
index 000000000..6ace59534
--- /dev/null
+++ b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx
@@ -0,0 +1,50 @@
+import * as React from 'react';
+import { NavLink, Link, Route } from 'react-router-dom';
+
+import ListItem from '@material-ui/core/ListItem';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import ListItemText from '@material-ui/core/ListItemText';
+
+import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
+
+const styles = (theme: Theme) => createStyles({
+ active: {
+ backgroundColor: theme.palette.action.selected
+ }
+});
+
+export interface IListItemLinkProps extends WithStyles<typeof styles> {
+ icon: JSX.Element | null;
+ primary: string | React.ComponentType;
+ secondary?: React.ComponentType;
+ to: string;
+ exact?: boolean;
+}
+
+export const ListItemLink = withStyles(styles)((props: IListItemLinkProps) => {
+ const { icon, primary: Primary, secondary: Secondary, classes, to, exact = false } = props;
+ const renderLink = (itemProps: any): JSX.Element => (<NavLink exact={ exact } to={ to } activeClassName={ classes.active } { ...itemProps } />);
+
+ return (
+ <>
+ <ListItem button component={ renderLink }>
+ { icon
+ ? <ListItemIcon>{ icon }</ListItemIcon>
+ : null
+ }
+ { typeof Primary === 'string'
+ ? <ListItemText primary={ Primary } style={{ padding: 0 }} />
+ : <Primary />
+ }
+ </ListItem>
+ { Secondary
+ ? <Route exact={ exact } path={ to } component={ Secondary } />
+ : null
+ }
+ </>
+ );
+ }
+);
+
+export default ListItemLink;
+
diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx
new file mode 100644
index 000000000..0b64666c0
--- /dev/null
+++ b/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx
@@ -0,0 +1,56 @@
+import * as React from 'react';
+
+import { withStyles, Theme, WithStyles, createStyles } from '@material-ui/core/styles';
+
+import { ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Typography, ExpansionPanelActions } from '@material-ui/core';
+
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import { SvgIconProps } from '@material-ui/core/SvgIcon';
+
+const styles = (theme: Theme) => createStyles({
+ accordion: {
+ // background: theme.palette.secondary.dark,
+ // color: theme.palette.primary.contrastText
+ },
+ detail: {
+ // background: theme.palette.background.paper,
+ // color: theme.palette.text.primary,
+ position: "relative",
+ display: 'flex',
+ flexDirection: 'column'
+ },
+ text: {
+ // color: theme.palette.common.white,
+ // fontSize: "1rem"
+ },
+});
+
+type PanalProps = WithStyles<typeof styles> & {
+ activePanel: string | null,
+ panelId: string,
+ title: string,
+ customActionButtons?: JSX.Element[];
+ onToggle: (panelId: string | null) => void;
+}
+
+const PanelComponent: React.SFC<PanalProps> = (props) => {
+ const { classes, activePanel, onToggle } = props;
+ return (
+ <ExpansionPanel className={ classes.accordion } expanded={ activePanel === props.panelId } onChange={ () => onToggle(props.panelId) } >
+ <ExpansionPanelSummary expandIcon={ <ExpandMoreIcon /> }>
+ <Typography className={ classes.text } >{ props.title }</Typography>
+ </ExpansionPanelSummary>
+ <ExpansionPanelDetails className={ classes.detail }>
+ { props.children }
+ </ExpansionPanelDetails>
+ { props.customActionButtons
+ ? <ExpansionPanelActions>
+ { props.customActionButtons }
+ </ExpansionPanelActions>
+ : null }
+ </ExpansionPanel>
+ );
+};
+
+export const Panel = withStyles(styles)(PanelComponent);
+export default Panel; \ No newline at end of file
diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx
new file mode 100644
index 000000000..c02bf93e9
--- /dev/null
+++ b/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx
@@ -0,0 +1,57 @@
+import * as React from 'react';
+
+import { IApplicationStoreState } from '../../store/applicationStore';
+import { Connect, connect, IDispatcher } from '../../flux/connect';
+import { RemoveSnackbarNotification } from '../../actions/snackbarActions';
+
+import { InjectedNotistackProps, withSnackbar } from 'notistack';
+
+const mapProps = (state: IApplicationStoreState) => ({
+ notifications: state.framework.applicationState.snackBars
+});
+
+const mapDispatch = (dispatcher: IDispatcher) => ({
+ removeSnackbar: (key: number) => {
+ dispatcher.dispatch(new RemoveSnackbarNotification(key));
+ }
+});
+
+type DisplaySnackbarsComponentProps = Connect<typeof mapProps, typeof mapDispatch> & InjectedNotistackProps;
+
+class DisplaySnackbarsComponent extends React.Component<DisplaySnackbarsComponentProps> {
+ private displayed: number[] = [];
+
+ private storeDisplayed = (id: number) => {
+ this.displayed = [...this.displayed, id];
+ };
+
+ public shouldComponentUpdate({ notifications: newSnacks = [] }: DisplaySnackbarsComponentProps) {
+
+ const { notifications: currentSnacks } = this.props;
+ let notExists = false;
+ for (let i = 0; i < newSnacks.length; i++) {
+ if (notExists) continue;
+ notExists = notExists || !currentSnacks.filter(({ key }) => newSnacks[i].key === key).length;
+ }
+ return notExists;
+ }
+
+ componentDidUpdate() {
+ const { notifications = [] } = this.props;
+
+ notifications.forEach(notification => {
+ if (this.displayed.includes(notification.key)) return;
+ const options = notification.options || {};
+ this.props.enqueueSnackbar(notification.message, options);
+ this.storeDisplayed(notification.key);
+ this.props.removeSnackbar(notification.key);
+ });
+ }
+
+ render() {
+ return null;
+ }
+}
+
+const DisplayStackbars = withSnackbar(connect(mapProps, mapDispatch)(DisplaySnackbarsComponent));
+export default DisplayStackbars; \ No newline at end of file
diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx
new file mode 100644
index 000000000..522ff12c8
--- /dev/null
+++ b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx
@@ -0,0 +1,159 @@
+
+import * as React from 'react';
+import classNames from 'classnames';
+import { withStyles, WithStyles, Theme, createStyles } from '@material-ui/core/styles';
+import { fade } from '@material-ui/core/styles/colorManipulator';
+import ButtonBase from '@material-ui/core/ButtonBase';
+
+
+export const styles = (theme: Theme) => createStyles({
+ /* Styles applied to the root element. */
+ root: {
+ ...theme.typography.button,
+ height: 32,
+ minWidth: 48,
+ margin: 0,
+ padding: `${theme.spacing.unit - 4}px ${theme.spacing.unit * 1.5}px`,
+ borderRadius: 2,
+ willChange: 'opacity',
+ color: fade(theme.palette.action.active, 0.38),
+ '&:hover': {
+ textDecoration: 'none',
+ // Reset on mouse devices
+ backgroundColor: fade(theme.palette.text.primary, 0.12),
+ '@media (hover: none)': {
+ backgroundColor: 'transparent',
+ },
+ '&$disabled': {
+ backgroundColor: 'transparent',
+ },
+ },
+ '&:not(:first-child)': {
+ borderTopLeftRadius: 0,
+ borderBottomLeftRadius: 0,
+ },
+ '&:not(:last-child)': {
+ borderTopRightRadius: 0,
+ borderBottomRightRadius: 0,
+ },
+ },
+ /* Styles applied to the root element if `disabled={true}`. */
+ disabled: {
+ color: fade(theme.palette.action.disabled, 0.12),
+ },
+ /* Styles applied to the root element if `selected={true}`. */
+ selected: {
+ color: theme.palette.action.active,
+ '&:after': {
+ content: '""',
+ display: 'block',
+ position: 'absolute',
+ overflow: 'hidden',
+ borderRadius: 'inherit',
+ width: '100%',
+ height: '100%',
+ left: 0,
+ top: 0,
+ pointerEvents: 'none',
+ zIndex: 0,
+ backgroundColor: 'currentColor',
+ opacity: 0.38,
+ },
+ '& + &:before': {
+ content: '""',
+ display: 'block',
+ position: 'absolute',
+ overflow: 'hidden',
+ width: 1,
+ height: '100%',
+ left: 0,
+ top: 0,
+ pointerEvents: 'none',
+ zIndex: 0,
+ backgroundColor: 'currentColor',
+ opacity: 0.12,
+ },
+ },
+ /* Styles applied to the `label` wrapper element. */
+ label: {
+ width: '100%',
+ display: 'inherit',
+ alignItems: 'inherit',
+ justifyContent: 'inherit',
+ },
+});
+
+export type ToggleButtonClassKey = 'disabled' | 'root' | 'label' | 'selected';
+
+interface IToggleButtonProps extends WithStyles<typeof styles> {
+ className?: string;
+ component?: React.ReactType<IToggleButtonProps>;
+ disabled?: boolean;
+ disableFocusRipple?: boolean;
+ disableRipple?: boolean;
+ selected?: boolean;
+ type?: string;
+ value?: any;
+ onClick?: (event: React.FormEvent<HTMLElement>, value?: any) => void;
+ onChange?: (event: React.FormEvent<HTMLElement>, value?: any) => void;
+}
+
+class ToggleButtonComponent extends React.Component<IToggleButtonProps> {
+ handleChange = (event: React.FormEvent<HTMLElement>) => {
+ const { onChange, onClick, value } = this.props;
+
+ if (onClick) {
+ onClick(event, value);
+ if (event.isDefaultPrevented()) {
+ return;
+ }
+ }
+
+ if (onChange) {
+ onChange(event, value);
+ }
+ };
+
+ render() {
+ const {
+ children,
+ className: classNameProp,
+ classes,
+ disableFocusRipple,
+ disabled,
+ selected,
+ ...other
+ } = this.props;
+
+ const className = classNames(
+ classes.root,
+ {
+ [classes.disabled]: disabled,
+ [classes.selected]: selected,
+ },
+ classNameProp,
+ );
+
+ return (
+ <ButtonBase
+ className={className}
+ disabled={disabled}
+ focusRipple={!disableFocusRipple}
+ onClick={this.handleChange}
+ {...other}
+ >
+ <span className={classes.label}>{children}</span>
+ </ButtonBase>
+ );
+ }
+ public static defaultProps = {
+ disabled: false,
+ disableFocusRipple: false,
+ disableRipple: false,
+ };
+
+ public static muiName = 'ToggleButton';
+}
+
+export const ToggleButton = withStyles(styles, { name: 'MuiToggleButton' })(ToggleButtonComponent);
+export default ToggleButton; \ No newline at end of file
diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx
new file mode 100644
index 000000000..8ab7c2b91
--- /dev/null
+++ b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react';
+import classNames from 'classnames';
+import { withStyles, WithStyles, Theme, createStyles } from '@material-ui/core/styles';
+
+export const styles = (theme: Theme) => createStyles({
+ /* Styles applied to the root element. */
+ root: {
+ transition: theme.transitions.create('background,box-shadow'),
+ background: 'transparent',
+ borderRadius: 2,
+ overflow: 'hidden',
+ },
+ /* Styles applied to the root element if `selected={true}` or `selected="auto" and `value` set. */
+ selected: {
+ background: theme.palette.background.paper,
+ boxShadow: theme.shadows[2],
+ },
+});
+
diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx
new file mode 100644
index 000000000..8bcdc8bc6
--- /dev/null
+++ b/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx
@@ -0,0 +1,251 @@
+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<SvgIconProps>;
+}
+
+type TreeViewComponentState<TData extends ITreeItem = ITreeItem> = {
+ /** 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<TData extends ITreeItem = ITreeItem> = WithTheme & {
+ items: TData[];
+ contentProperty: keyof Omit<TData, keyof ITreeItem>;
+ childrenProperty: keyof Omit<TData, keyof ITreeItem>;
+ useFolderIcons?: boolean;
+ enableSearchBar?: boolean;
+ autoExpandFolder?: boolean;
+ style?: React.CSSProperties;
+ itemHeight?: number;
+ depthOffset?: number;
+}
+
+type TreeViewComponentWithInternalStateProps<TData extends ITreeItem = ITreeItem> = TreeViewComponentBaseProps<TData> & {
+ onItemClick?: (item: TData) => void;
+ onFolderClick?: (item: TData) => void;
+}
+
+type TreeViewComponentWithExternalStateProps<TData extends ITreeItem = ITreeItem> = TreeViewComponentBaseProps<TData> & TreeViewComponentState<TData> & {
+ onSearch: (searchTerm: string) => void;
+ onItemClick: (item: TData) => void;
+ onFolderClick: (item: TData) => void;
+}
+
+type TreeViewComponentProps<TData extends ITreeItem = ITreeItem> =
+ TreeViewComponentWithInternalStateProps<TData> |
+ TreeViewComponentWithExternalStateProps<TData>;
+
+function isTreeViewComponentWithExternalStateProps<TData extends ITreeItem = ITreeItem>(props: TreeViewComponentProps<TData>): props is TreeViewComponentWithExternalStateProps<TData> {
+ const propsWithExternalState = (props as TreeViewComponentWithExternalStateProps<TData>)
+ return (
+ propsWithExternalState.onSearch instanceof Function ||
+ propsWithExternalState.expandedItems !== undefined ||
+ propsWithExternalState.activeItem !== undefined ||
+ propsWithExternalState.searchTerm !== undefined
+ );
+}
+
+class TreeViewComponent<TData extends ITreeItem> extends React.Component<TreeViewComponentProps<TData>, TreeViewComponentState> {
+
+ /**
+ * Initializes a new instance.
+ */
+ constructor(props: TreeViewComponentProps<TData>) {
+ 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 (
+ <div style={ styles.root }>
+ { children }
+ { enableSearchBar && <TextField label={ "Search" } fullWidth={ true } style={ styles.search } value={ searchTerm } onChange={ this.onChangeSearchText } /> || null }
+ <List>
+ { this.renderItems(items, searchTerm && searchTerm.toLowerCase()) }
+ </List>
+ </div>
+ );
+ }
+
+ 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)
+ ? (
+ <ListItem key={ `tree-list-${ this.itemIndex++ }` } style={ styles.item } onClick={ handleClickCreator(false) } button >
+
+ { // display the left icon
+ (this.props.useFolderIcons && <ListItemIcon>{ isFolder ? <FolderIcon /> : <FileIcon /> }</ListItemIcon>) ||
+ (item.icon && (<ListItemIcon><item.icon /></ListItemIcon>)) }
+
+
+ { // highlight search result
+ matchIndex > -1
+ ? (<span>
+ { text.substring(0, matchIndex) }
+ <span
+ style={ {
+ display: 'inline-block',
+ backgroundColor: 'rgba(255,235,59,0.5)',
+ padding: '3px',
+ } }
+ >
+ { text.substring(matchIndex, matchIndex + searchTermLength) }
+ </span>
+ { text.substring(matchIndex + searchTermLength) }
+ </span>)
+ : (<ListItemText primary={ text } />)
+ }
+
+ { // display the right icon, depending on the state
+ !isFolder ? null : expanded ? (<OpenIcon onClick={ handleClickCreator(true) } />) : (<CloseIcon onClick={ handleClickCreator(true) } />) }
+ </ListItem>
+ )
+ : 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<HTMLInputElement>) => {
+ 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<TData extends ITreeItem = ITreeItem> = new () => React.Component<Omit<TreeViewComponentProps<TData>, 'theme'>>;
+
+export const TreeView = withTheme()(TreeViewComponent);
+export default TreeView; \ No newline at end of file