Merge branch 'master' of git.curoverse.com:arvados-workbench2 into 13827-select-field...
[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 * as 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     value: Value[];
13     getLabel?: (value: Value) => string;
14     onChange: (value: Value[]) => void;
15     createNewValue: (value: string) => Value;
16     inputComponent?: React.ComponentType<InputProps>;
17     inputProps?: InputProps;
18     deletable?: boolean;
19     orderable?: boolean;
20 }
21
22 type CssRules = 'chips' | 'input' | 'inputContainer';
23
24 const styles: StyleRulesCallback = ({ spacing }) => ({
25     chips: {
26         minHeight: spacing.unit * 5,
27         zIndex: 1,
28         position: 'relative',
29     },
30     input: {
31         zIndex: 1,
32         marginBottom: 8,
33         position: 'relative',
34     },
35     inputContainer: {
36         marginTop: -34
37     },
38 });
39
40 export const ChipsInput = withStyles(styles)(
41     class ChipsInput<Value> extends React.Component<ChipsInputProps<Value> & WithStyles<CssRules>> {
42
43         state = {
44             text: '',
45         };
46
47         filler = React.createRef<HTMLDivElement>();
48         timeout = -1;
49
50         setText = (event: React.ChangeEvent<HTMLInputElement>) => {
51             this.setState({ text: event.target.value });
52         }
53
54         handleKeyPress = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
55             if (key === 'Enter') {
56                 this.createNewValue();
57             } else if (key === 'Backspace') {
58                 this.deleteLastValue();
59             }
60         }
61
62         createNewValue = () => {
63             if (this.state.text) {
64                 const newValue = this.props.createNewValue(this.state.text);
65                 this.setState({ text: '' });
66                 this.props.onChange([...this.props.value, newValue]);
67             }
68         }
69
70         deleteLastValue = () => {
71             if (this.state.text.length === 0 && this.props.value.length > 0) {
72                 this.props.onChange(this.props.value.slice(0, -1));
73             }
74         }
75
76         updateCursorPosition = () => {
77             if (this.timeout) {
78                 clearTimeout(this.timeout);
79             }
80             this.timeout = setTimeout(() => this.setState({ ...this.state }));
81         }
82
83         getInputStyles = (): React.CSSProperties => ({
84             width: this.filler.current
85                 ? this.filler.current.offsetWidth
86                 : '100%',
87             right: this.filler.current
88                 ? `calc(${this.filler.current.offsetWidth}px - 100%)`
89                 : 0,
90
91         })
92
93         componentDidMount() {
94             this.updateCursorPosition();
95         }
96
97         render() {
98             return <>
99                 {this.renderChips()}
100                 {this.renderInput()}
101             </>;
102         }
103
104         renderChips() {
105             const { classes, value, ...props } = this.props;
106             return <div className={classes.chips}>
107                 <Chips
108                     {...props}
109                     values={value}
110                     filler={<div ref={this.filler} />}
111                 />
112             </div>;
113         }
114
115         renderInput() {
116             const { inputProps: InputProps, inputComponent: Input = MuiInput, classes } = this.props;
117             return <Input
118                 {...InputProps}
119                 value={this.state.text}
120                 onChange={this.setText}
121                 onKeyDown={this.handleKeyPress}
122                 inputProps={{
123                     ...(InputProps && InputProps.inputProps),
124                     className: classes.input,
125                     style: this.getInputStyles(),
126                 }}
127                 fullWidth
128                 className={classes.inputContainer} />;
129         }
130
131         componentDidUpdate(prevProps: ChipsInputProps<Value>) {
132             if (prevProps.value !== this.props.value) {
133                 this.updateCursorPosition();
134             }
135         }
136         componentWillUnmount() {
137             clearTimeout(this.timeout);
138         }
139     });