31e123330e3570262369942a8a32b60b25aa33d1
[arvados-workbench2.git] / src / components / chips-input / chips-input.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { Chips } from 'components/chips/chips';
7 import { Input as MuiInput, withStyles, WithStyles } from '@material-ui/core';
8 import { StyleRulesCallback } from '@material-ui/core/styles';
9 import { InputProps } from '@material-ui/core/Input';
10
11 interface ChipsInputProps<Value> {
12     values: Value[];
13     getLabel?: (value: Value) => string;
14     onChange: (value: Value[]) => void;
15     handleFocus?: (e: any) => void;
16     handleBlur?: (e: any) => void;
17     chipsClassName?: string;
18     createNewValue: (value: string) => Value;
19     inputComponent?: React.ComponentType<InputProps>;
20     inputProps?: InputProps;
21     deletable?: boolean;
22     orderable?: boolean;
23     disabled?: boolean;
24 }
25
26 type CssRules = 'chips' | 'input' | 'inputContainer';
27
28 const styles: StyleRulesCallback = ({ spacing }) => ({
29     chips: {
30         minHeight: spacing.unit * 5,
31         zIndex: 1,
32         position: 'relative',
33     },
34     input: {
35         zIndex: 1,
36         marginBottom: 8,
37         position: 'relative',
38     },
39     inputContainer: {
40         marginTop: -34
41     },
42 });
43
44 export const ChipsInput = withStyles(styles)(
45     class ChipsInput<Value> extends React.Component<ChipsInputProps<Value> & WithStyles<CssRules>> {
46
47         state = {
48             text: '',
49         };
50
51         filler = React.createRef<HTMLDivElement>();
52         timeout = -1;
53
54         setText = (event: React.ChangeEvent<HTMLInputElement>) => {
55             this.setState({ text: event.target.value });
56         }
57
58         handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
59             if (e.key === 'Enter') {
60                 this.createNewValue();
61                 e.preventDefault();
62             } else if (e.key === 'Backspace') {
63                 this.deleteLastValue();
64             }
65         }
66
67         createNewValue = () => {
68             if (this.state.text) {
69                 const newValue = this.props.createNewValue(this.state.text);
70                 this.setState({ text: '' });
71                 this.props.onChange([...this.props.values, newValue]);
72             }
73         }
74
75         deleteLastValue = () => {
76             if (this.state.text.length === 0 && this.props.values.length > 0) {
77                 this.props.onChange(this.props.values.slice(0, -1));
78             }
79         }
80
81         updateCursorPosition = () => {
82             if (this.timeout) {
83                 clearTimeout(this.timeout);
84             }
85             this.timeout = window.setTimeout(() => this.setState({ ...this.state }));
86         }
87
88         getInputStyles = (): React.CSSProperties => ({
89             width: this.filler.current
90                 ? this.filler.current.offsetWidth
91                 : '100%',
92             right: this.filler.current
93                 ? `calc(${this.filler.current.offsetWidth}px - 100%)`
94                 : 0,
95
96         })
97
98         componentDidMount() {
99             this.updateCursorPosition();
100         }
101
102         render() {
103             return <>
104                 {this.renderChips()}
105                 {this.renderInput()}
106             </>;
107         }
108
109         renderChips() {
110             const { classes, ...props } = this.props;
111             return <div className={[classes.chips, this.props.chipsClassName].join(' ')}>
112                 <Chips
113                     {...props}
114                     clickable={!props.disabled}
115                     filler={<div ref={this.filler} />}
116                 />
117             </div>;
118         }
119
120         renderInput() {
121             const { inputProps: InputProps, inputComponent: Input = MuiInput, classes } = this.props;
122             return <Input
123                 {...InputProps}
124                 value={this.state.text}
125                 onChange={this.setText}
126                 disabled={this.props.disabled}
127                 onKeyDown={this.handleKeyPress}
128                 onFocus={this.props.handleFocus}
129                 onBlur={this.props.handleBlur}
130                 inputProps={{
131                     ...(InputProps && InputProps.inputProps),
132                     className: classes.input,
133                     style: this.getInputStyles(),
134                 }}
135                 fullWidth
136                 className={classes.inputContainer} />;
137         }
138
139         componentDidUpdate(prevProps: ChipsInputProps<Value>) {
140             if (prevProps.values !== this.props.values) {
141                 this.updateCursorPosition();
142             }
143         }
144         componentWillUnmount() {
145             clearTimeout(this.timeout);
146         }
147     });