ExpressionInput.tsx 6.4 KB
Newer Older
1
import React, { Component } from 'react';
2
import { Button, InputGroup, InputGroupAddon, InputGroupText, Input } from 'reactstrap';
3

B
Boyko 已提交
4
import Downshift, { ControllerStateAndHelpers } from 'downshift';
5
import fuzzy from 'fuzzy';
6
import SanitizeHTML from './components/SanitizeHTML';
7 8 9 10 11 12

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';

interface ExpressionInputProps {
  value: string;
13
  autocompleteSections: { [key: string]: string[] };
14 15 16 17
  executeQuery: (expr: string) => void;
  loading: boolean;
}

B
Boyko 已提交
18 19 20 21 22 23
interface ExpressionInputState {
  height: number | string;
  value: string;
}

class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputState> {
24 25
  private exprInputRef = React.createRef<HTMLInputElement>();

B
Boyko 已提交
26 27 28 29
  constructor(props: ExpressionInputProps) {
    super(props);
    this.state = {
      value: props.value,
C
CSTDev 已提交
30 31
      height: 'auto',
    };
B
Boyko 已提交
32 33 34 35 36 37 38 39 40 41
  }

  componentDidMount() {
    this.setHeight();
  }

  setHeight = () => {
    const { offsetHeight, clientHeight, scrollHeight } = this.exprInputRef.current!;
    const offset = offsetHeight - clientHeight; // Needed in order for the height to be more accurate.
    this.setState({ height: scrollHeight + offset });
C
CSTDev 已提交
42
  };
B
Boyko 已提交
43 44

  handleInput = () => {
C
CSTDev 已提交
45 46 47 48 49 50 51 52
    this.setState(
      {
        height: 'auto',
        value: this.exprInputRef.current!.value,
      },
      this.setHeight
    );
  };
B
Boyko 已提交
53

B
Boyko 已提交
54
  handleDropdownSelection = (value: string) => {
55
    this.setState({ value, height: 'auto' }, this.setHeight);
B
Boyko 已提交
56 57
  };

58 59
  handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
    if (event.key === 'Enter' && !event.shiftKey) {
60
      this.executeQuery();
61 62
      event.preventDefault();
    }
C
CSTDev 已提交
63
  };
64

65
  executeQuery = () => this.props.executeQuery(this.exprInputRef.current!.value);
66

67 68
  getSearchMatches = (input: string, expressions: string[]) => {
    return fuzzy.filter(input.replace(/ /g, ''), expressions, {
C
CSTDev 已提交
69 70
      pre: '<strong>',
      post: '</strong>',
71
    });
C
CSTDev 已提交
72
  };
73

74 75 76 77
  createAutocompleteSection = (downshift: ControllerStateAndHelpers<any>) => {
    const { inputValue = '', closeMenu, highlightedIndex } = downshift;
    const { autocompleteSections } = this.props;
    let index = 0;
C
CSTDev 已提交
78 79 80 81 82 83 84 85
    const sections = inputValue!.length
      ? Object.entries(autocompleteSections).reduce(
          (acc, [title, items]) => {
            const matches = this.getSearchMatches(inputValue!, items);
            return !matches.length
              ? acc
              : [
                  ...acc,
86 87
                  <ul className="autosuggest-dropdown-list" key={title}>
                    <li className="autosuggest-dropdown-header">{title}</li>
C
CSTDev 已提交
88 89 90 91 92 93 94 95 96 97 98 99
                    {matches
                      .slice(0, 100) // Limit DOM rendering to 100 results, as DOM rendering is sloooow.
                      .map(({ original, string }) => {
                        const itemProps = downshift.getItemProps({
                          key: original,
                          index,
                          item: original,
                          style: {
                            backgroundColor: highlightedIndex === index++ ? 'lightgray' : 'white',
                          },
                        });
                        return (
100
                          <SanitizeHTML tag="li" {...itemProps} allowedTags={['strong']}>
C
CSTDev 已提交
101 102 103 104
                            {string}
                          </SanitizeHTML>
                        );
                      })}
105
                  </ul>,
C
CSTDev 已提交
106 107 108 109 110
                ];
          },
          [] as JSX.Element[]
        )
      : [];
111 112 113 114 115

    if (!sections.length) {
      // This is ugly but is needed in order to sync state updates.
      // This way we force downshift to wait React render call to complete before closeMenu to be triggered.
      setTimeout(closeMenu);
116 117 118 119
      return null;
    }

    return (
120
      <div {...downshift.getMenuProps()} className="autosuggest-dropdown">
C
CSTDev 已提交
121
        {sections}
122
      </div>
123
    );
C
CSTDev 已提交
124
  };
125 126

  render() {
B
Boyko 已提交
127
    const { value, height } = this.state;
128
    return (
129
      <Downshift onSelect={this.handleDropdownSelection}>
C
CSTDev 已提交
130
        {downshift => (
131 132 133 134
          <div>
            <InputGroup className="expression-input">
              <InputGroupAddon addonType="prepend">
                <InputGroupText>
B
Boyko 已提交
135
                  {this.props.loading ? <FontAwesomeIcon icon={faSpinner} spin /> : <FontAwesomeIcon icon={faSearch} />}
136 137 138
                </InputGroupText>
              </InputGroupAddon>
              <Input
B
Boyko 已提交
139 140
                onInput={this.handleInput}
                style={{ height }}
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
                autoFocus
                type="textarea"
                rows="1"
                onKeyPress={this.handleKeyPress}
                placeholder="Expression (press Shift+Enter for newlines)"
                innerRef={this.exprInputRef}
                {...downshift.getInputProps({
                  onKeyDown: (event: React.KeyboardEvent): void => {
                    switch (event.key) {
                      case 'Home':
                      case 'End':
                        // We want to be able to jump to the beginning/end of the input field.
                        // By default, Downshift otherwise jumps to the first/last suggestion item instead.
                        (event.nativeEvent as any).preventDownshiftDefault = true;
                        break;
                      case 'ArrowUp':
                      case 'ArrowDown':
                        if (!downshift.isOpen) {
159
                          (event.nativeEvent as any).preventDownshiftDefault = true;
160 161 162 163 164 165 166 167 168 169 170
                        }
                        break;
                      case 'Enter':
                        downshift.closeMenu();
                        break;
                      case 'Escape':
                        if (!downshift.isOpen) {
                          this.exprInputRef.current!.blur();
                        }
                        break;
                      default:
171
                    }
C
CSTDev 已提交
172
                  },
173
                } as any)}
174
                value={value}
175 176
              />
              <InputGroupAddon addonType="append">
C
CSTDev 已提交
177
                <Button className="execute-btn" color="primary" onClick={this.executeQuery}>
178 179 180 181
                  Execute
                </Button>
              </InputGroupAddon>
            </InputGroup>
182
            {downshift.isOpen && this.createAutocompleteSection(downshift)}
183 184 185
          </div>
        )}
      </Downshift>
186 187 188 189 190
    );
  }
}

export default ExpressionInput;