18284: Add regex pattern option to chips-input for better tokenization of login group...
[arvados-workbench2.git] / src / components / chips-input / chips-input.tsx
index 13699660053824a8f205efc1f8a556ba43d8b106..cbb1fb1283b31246152f2c32b6a961cb5f256248 100644 (file)
@@ -2,22 +2,26 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as React from 'react';
-import { Chips } from '~/components/chips/chips';
+import React from 'react';
+import { Chips } from 'components/chips/chips';
 import { Input as MuiInput, withStyles, WithStyles } from '@material-ui/core';
 import { StyleRulesCallback } from '@material-ui/core/styles';
 import { InputProps } from '@material-ui/core/Input';
 
 interface ChipsInputProps<Value> {
-    value: Value[];
+    values: Value[];
     getLabel?: (value: Value) => string;
     onChange: (value: Value[]) => void;
+    handleFocus?: (e: any) => void;
+    handleBlur?: (e: any) => void;
+    chipsClassName?: string;
     createNewValue: (value: string) => Value;
     inputComponent?: React.ComponentType<InputProps>;
     inputProps?: InputProps;
     deletable?: boolean;
     orderable?: boolean;
     disabled?: boolean;
+    pattern?: RegExp;
 }
 
 type CssRules = 'chips' | 'input' | 'inputContainer';
@@ -49,28 +53,51 @@ export const ChipsInput = withStyles(styles)(
         timeout = -1;
 
         setText = (event: React.ChangeEvent<HTMLInputElement>) => {
-            this.setState({ text: event.target.value });
+            this.setState({ text: event.target.value }, () => {
+                // If pattern is provided, check for delimiter
+                if (this.props.pattern) {
+                    const matches = this.state.text.match(this.props.pattern);
+                    // Only create values if 1 match and the last character is a delimiter
+                    //   (user pressed an invalid character at the end of a token)
+                    //   or if multiple matches (user pasted text)
+                    if (matches &&
+                            (
+                                matches.length > 1 ||
+                                (matches.length === 1 && !this.state.text.endsWith(matches[0]))
+                            )) {
+                        this.createNewValue(matches.map((i) => i));
+                    }
+                }
+            });
         }
 
-        handleKeyPress = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
-            if (key === 'Enter') {
+        handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
+            // Handle special keypresses
+            if (e.key === 'Enter') {
                 this.createNewValue();
-            } else if (key === 'Backspace') {
+                e.preventDefault();
+            } else if (e.key === 'Backspace') {
                 this.deleteLastValue();
             }
         }
 
-        createNewValue = () => {
+        createNewValue = (matches?: string[]) => {
             if (this.state.text) {
-                const newValue = this.props.createNewValue(this.state.text);
-                this.setState({ text: '' });
-                this.props.onChange([...this.props.value, newValue]);
+                if (matches && matches.length > 0) {
+                    const newValues = matches.map((v) => this.props.createNewValue(v));
+                    this.setState({ text: '' });
+                    this.props.onChange([...this.props.values, ...newValues]);
+                } else {
+                    const newValue = this.props.createNewValue(this.state.text);
+                    this.setState({ text: '' });
+                    this.props.onChange([...this.props.values, newValue]);
+                }
             }
         }
 
         deleteLastValue = () => {
-            if (this.state.text.length === 0 && this.props.value.length > 0) {
-                this.props.onChange(this.props.value.slice(0, -1));
+            if (this.state.text.length === 0 && this.props.values.length > 0) {
+                this.props.onChange(this.props.values.slice(0, -1));
             }
         }
 
@@ -78,7 +105,7 @@ export const ChipsInput = withStyles(styles)(
             if (this.timeout) {
                 clearTimeout(this.timeout);
             }
-            this.timeout = setTimeout(() => this.setState({ ...this.state }));
+            this.timeout = window.setTimeout(() => this.setState({ ...this.state }));
         }
 
         getInputStyles = (): React.CSSProperties => ({
@@ -103,12 +130,11 @@ export const ChipsInput = withStyles(styles)(
         }
 
         renderChips() {
-            const { classes, value, ...props } = this.props;
-            return <div className={classes.chips}>
+            const { classes, ...props } = this.props;
+            return <div className={[classes.chips, this.props.chipsClassName].join(' ')}>
                 <Chips
                     {...props}
                     clickable={!props.disabled}
-                    values={value}
                     filler={<div ref={this.filler} />}
                 />
             </div>;
@@ -122,6 +148,8 @@ export const ChipsInput = withStyles(styles)(
                 onChange={this.setText}
                 disabled={this.props.disabled}
                 onKeyDown={this.handleKeyPress}
+                onFocus={this.props.handleFocus}
+                onBlur={this.props.handleBlur}
                 inputProps={{
                     ...(InputProps && InputProps.inputProps),
                     className: classes.input,
@@ -132,7 +160,7 @@ export const ChipsInput = withStyles(styles)(
         }
 
         componentDidUpdate(prevProps: ChipsInputProps<Value>) {
-            if (prevProps.value !== this.props.value) {
+            if (prevProps.values !== this.props.values) {
                 this.updateCursorPosition();
             }
         }