1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from 'react';
6 import { Chips } from 'components/chips/chips';
7 import { Input as MuiInput } from '@mui/material';
8 import { WithStyles } from '@mui/styles';
9 import withStyles from '@mui/styles/withStyles';
10 import { CustomStyleRulesCallback } from 'common/custom-theme';
11 import { InputProps } from '@mui/material/Input';
13 interface ChipsInputProps<Value> {
15 getLabel?: (value: Value) => string;
16 onChange: (value: Value[]) => void;
17 onPartialInput?: (value: boolean) => void;
18 handleFocus?: (e: any) => void;
19 handleBlur?: (e: any) => void;
20 chipsClassName?: string;
21 createNewValue: (value: string) => Value;
22 inputComponent?: React.ComponentType<InputProps>;
23 inputProps?: InputProps;
30 type CssRules = 'chips' | 'input' | 'inputContainer';
32 const styles: CustomStyleRulesCallback = ({ spacing }) => ({
34 minHeight: spacing(5),
48 export const ChipsInput = withStyles(styles)(
49 class ChipsInput<Value> extends React.Component<ChipsInputProps<Value> & WithStyles<CssRules>> {
55 filler = React.createRef<HTMLDivElement>();
58 setText = (event: React.ChangeEvent<HTMLInputElement>) => {
59 this.setState({ text: event.target.value }, () => {
60 // Update partial input status
61 this.props.onPartialInput && this.props.onPartialInput(this.state.text !== '');
63 // If pattern is provided, check for delimiter
64 if (this.props.pattern) {
65 const matches = this.state.text.match(this.props.pattern);
66 // Only create values if 1 match and the last character is a delimiter
67 // (user pressed an invalid character at the end of a token)
68 // or if multiple matches (user pasted text)
72 (matches.length === 1 && !this.state.text.endsWith(matches[0]))
74 this.createNewValue(matches.map((i) => i));
80 handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
81 // Handle special keypresses
82 if (e.key === 'Enter') {
83 this.createNewValue();
85 } else if (e.key === 'Backspace') {
86 this.deleteLastValue();
90 createNewValue = (matches?: string[]) => {
91 if (this.state.text) {
92 if (matches && matches.length > 0) {
93 const newValues = matches.map((v) => this.props.createNewValue(v));
94 this.setState({ text: '' });
95 this.props.onChange([...this.props.values, ...newValues]);
97 const newValue = this.props.createNewValue(this.state.text);
98 this.setState({ text: '' });
99 this.props.onChange([...this.props.values, newValue]);
101 this.props.onPartialInput && this.props.onPartialInput(false);
105 deleteLastValue = () => {
106 if (this.state.text.length === 0 && this.props.values.length > 0) {
107 this.props.onChange(this.props.values.slice(0, -1));
111 updateCursorPosition = () => {
113 clearTimeout(this.timeout);
115 this.timeout = window.setTimeout(() => this.setState({ ...this.state }));
118 getInputStyles = (): React.CSSProperties => ({
119 width: this.filler.current
120 ? this.filler.current.offsetWidth
122 right: this.filler.current
123 ? `calc(${this.filler.current.offsetWidth}px - 100%)`
128 componentDidMount() {
129 this.updateCursorPosition();
140 const { classes, ...props } = this.props;
141 return <div className={[classes.chips, this.props.chipsClassName].join(' ')}>
144 clickable={!props.disabled}
145 filler={<div ref={this.filler} />}
151 const { inputProps: InputProps, inputComponent: Input = MuiInput, classes } = this.props;
154 value={this.state.text}
155 onChange={this.setText}
156 disabled={this.props.disabled}
157 onKeyDown={this.handleKeyPress}
158 onFocus={this.props.handleFocus}
159 onBlur={this.props.handleBlur}
161 ...(InputProps && InputProps.inputProps),
162 className: classes.input,
163 style: this.getInputStyles(),
166 className={classes.inputContainer} />;
169 componentDidUpdate(prevProps: ChipsInputProps<Value>) {
170 if (prevProps.values !== this.props.values) {
171 this.updateCursorPosition();
174 componentWillUnmount() {
175 clearTimeout(this.timeout);