[d24f17c] | 1 | import React from 'react';
|
---|
| 2 | import PropTypes from 'prop-types';
|
---|
| 3 | import debounce from 'lodash.debounce';
|
---|
| 4 |
|
---|
| 5 |
|
---|
| 6 | export class DebounceInput extends React.PureComponent {
|
---|
| 7 | static propTypes = {
|
---|
| 8 | element: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
---|
| 9 | type: PropTypes.string,
|
---|
| 10 | onChange: PropTypes.func.isRequired,
|
---|
| 11 | onKeyDown: PropTypes.func,
|
---|
| 12 | onBlur: PropTypes.func,
|
---|
| 13 | value: PropTypes.oneOfType([
|
---|
| 14 | PropTypes.string,
|
---|
| 15 | PropTypes.number
|
---|
| 16 | ]),
|
---|
| 17 | minLength: PropTypes.number,
|
---|
| 18 | debounceTimeout: PropTypes.number,
|
---|
| 19 | forceNotifyByEnter: PropTypes.bool,
|
---|
| 20 | forceNotifyOnBlur: PropTypes.bool,
|
---|
| 21 | inputRef: PropTypes.func
|
---|
| 22 | };
|
---|
| 23 |
|
---|
| 24 |
|
---|
| 25 | static defaultProps = {
|
---|
| 26 | element: 'input',
|
---|
| 27 | type: 'text',
|
---|
| 28 | onKeyDown: undefined,
|
---|
| 29 | onBlur: undefined,
|
---|
| 30 | value: undefined,
|
---|
| 31 | minLength: 0,
|
---|
| 32 | debounceTimeout: 100,
|
---|
| 33 | forceNotifyByEnter: true,
|
---|
| 34 | forceNotifyOnBlur: true,
|
---|
| 35 | inputRef: undefined
|
---|
| 36 | };
|
---|
| 37 |
|
---|
| 38 |
|
---|
| 39 | constructor(props) {
|
---|
| 40 | super(props);
|
---|
| 41 |
|
---|
| 42 | this.isDebouncing = false;
|
---|
| 43 | this.state = {value: typeof props.value === 'undefined' || props.value === null ? '' : props.value};
|
---|
| 44 |
|
---|
| 45 | const {debounceTimeout} = this.props;
|
---|
| 46 | this.createNotifier(debounceTimeout);
|
---|
| 47 | }
|
---|
| 48 |
|
---|
| 49 |
|
---|
| 50 | componentDidUpdate(prevProps) {
|
---|
| 51 | if (this.isDebouncing) {
|
---|
| 52 | return;
|
---|
| 53 | }
|
---|
| 54 | const {value, debounceTimeout} = this.props;
|
---|
| 55 |
|
---|
| 56 | const {debounceTimeout: oldTimeout, value: oldValue} = prevProps;
|
---|
| 57 | const {value: stateValue} = this.state;
|
---|
| 58 |
|
---|
| 59 | if (typeof value !== 'undefined' && oldValue !== value && stateValue !== value) {
|
---|
| 60 | // Update state.value if new value passed via props, yep re-render should happen
|
---|
| 61 | // eslint-disable-next-line react/no-did-update-set-state
|
---|
| 62 | this.setState({value});
|
---|
| 63 | }
|
---|
| 64 | if (debounceTimeout !== oldTimeout) {
|
---|
| 65 | this.createNotifier(debounceTimeout);
|
---|
| 66 | }
|
---|
| 67 | }
|
---|
| 68 |
|
---|
| 69 |
|
---|
| 70 | componentWillUnmount() {
|
---|
| 71 | if (this.flush) {
|
---|
| 72 | this.flush();
|
---|
| 73 | }
|
---|
| 74 | }
|
---|
| 75 |
|
---|
| 76 |
|
---|
| 77 | onChange = event => {
|
---|
| 78 | event.persist();
|
---|
| 79 |
|
---|
| 80 | const {value: oldValue} = this.state;
|
---|
| 81 | const {minLength} = this.props;
|
---|
| 82 |
|
---|
| 83 | this.setState({value: event.target.value}, () => {
|
---|
| 84 | const {value} = this.state;
|
---|
| 85 |
|
---|
| 86 | if (value.length >= minLength) {
|
---|
| 87 | this.notify(event);
|
---|
| 88 | return;
|
---|
| 89 | }
|
---|
| 90 |
|
---|
| 91 | // If user hits backspace and goes below minLength consider it cleaning the value
|
---|
| 92 | if (oldValue.length > value.length) {
|
---|
| 93 | this.notify({...event, target: {...event.target, value: ''}});
|
---|
| 94 | }
|
---|
| 95 | });
|
---|
| 96 | };
|
---|
| 97 |
|
---|
| 98 |
|
---|
| 99 | onKeyDown = event => {
|
---|
| 100 | if (event.key === 'Enter') {
|
---|
| 101 | this.forceNotify(event);
|
---|
| 102 | }
|
---|
| 103 | // Invoke original onKeyDown if present
|
---|
| 104 | const {onKeyDown} = this.props;
|
---|
| 105 | if (onKeyDown) {
|
---|
| 106 | event.persist();
|
---|
| 107 | onKeyDown(event);
|
---|
| 108 | }
|
---|
| 109 | };
|
---|
| 110 |
|
---|
| 111 |
|
---|
| 112 | onBlur = event => {
|
---|
| 113 | this.forceNotify(event);
|
---|
| 114 | // Invoke original onBlur if present
|
---|
| 115 | const {onBlur} = this.props;
|
---|
| 116 | if (onBlur) {
|
---|
| 117 | event.persist();
|
---|
| 118 | onBlur(event);
|
---|
| 119 | }
|
---|
| 120 | };
|
---|
| 121 |
|
---|
| 122 |
|
---|
| 123 | createNotifier = debounceTimeout => {
|
---|
| 124 | if (debounceTimeout < 0) {
|
---|
| 125 | this.notify = () => null;
|
---|
| 126 | } else if (debounceTimeout === 0) {
|
---|
| 127 | this.notify = this.doNotify;
|
---|
| 128 | } else {
|
---|
| 129 | const debouncedChangeFunc = debounce(event => {
|
---|
| 130 | this.isDebouncing = false;
|
---|
| 131 | this.doNotify(event);
|
---|
| 132 | }, debounceTimeout);
|
---|
| 133 |
|
---|
| 134 | this.notify = event => {
|
---|
| 135 | this.isDebouncing = true;
|
---|
| 136 | debouncedChangeFunc(event);
|
---|
| 137 | };
|
---|
| 138 |
|
---|
| 139 | this.flush = () => debouncedChangeFunc.flush();
|
---|
| 140 |
|
---|
| 141 | this.cancel = () => {
|
---|
| 142 | this.isDebouncing = false;
|
---|
| 143 | debouncedChangeFunc.cancel();
|
---|
| 144 | };
|
---|
| 145 | }
|
---|
| 146 | };
|
---|
| 147 |
|
---|
| 148 |
|
---|
| 149 | doNotify = (...args) => {
|
---|
| 150 | const {onChange} = this.props;
|
---|
| 151 |
|
---|
| 152 | onChange(...args);
|
---|
| 153 | };
|
---|
| 154 |
|
---|
| 155 |
|
---|
| 156 | forceNotify = event => {
|
---|
| 157 | const {debounceTimeout} = this.props;
|
---|
| 158 | if (!this.isDebouncing && debounceTimeout > 0) {
|
---|
| 159 | return;
|
---|
| 160 | }
|
---|
| 161 |
|
---|
| 162 | if (this.cancel) {
|
---|
| 163 | this.cancel();
|
---|
| 164 | }
|
---|
| 165 |
|
---|
| 166 | const {value} = this.state;
|
---|
| 167 | const {minLength} = this.props;
|
---|
| 168 |
|
---|
| 169 | if (value.length >= minLength) {
|
---|
| 170 | this.doNotify(event);
|
---|
| 171 | } else {
|
---|
| 172 | this.doNotify({...event, target: {...event.target, value}});
|
---|
| 173 | }
|
---|
| 174 | };
|
---|
| 175 |
|
---|
| 176 |
|
---|
| 177 | render() {
|
---|
| 178 | const {
|
---|
| 179 | element,
|
---|
| 180 | onChange: _onChange,
|
---|
| 181 | value: _value,
|
---|
| 182 | minLength: _minLength,
|
---|
| 183 | debounceTimeout: _debounceTimeout,
|
---|
| 184 | forceNotifyByEnter,
|
---|
| 185 | forceNotifyOnBlur,
|
---|
| 186 | onKeyDown,
|
---|
| 187 | onBlur,
|
---|
| 188 | inputRef,
|
---|
| 189 | ...props
|
---|
| 190 | } = this.props;
|
---|
| 191 | const {value} = this.state;
|
---|
| 192 |
|
---|
| 193 | let maybeOnKeyDown;
|
---|
| 194 | if (forceNotifyByEnter) {
|
---|
| 195 | maybeOnKeyDown = {onKeyDown: this.onKeyDown};
|
---|
| 196 | } else if (onKeyDown) {
|
---|
| 197 | maybeOnKeyDown = {onKeyDown};
|
---|
| 198 | } else {
|
---|
| 199 | maybeOnKeyDown = {};
|
---|
| 200 | }
|
---|
| 201 |
|
---|
| 202 | let maybeOnBlur;
|
---|
| 203 | if (forceNotifyOnBlur) {
|
---|
| 204 | maybeOnBlur = {onBlur: this.onBlur};
|
---|
| 205 | } else if (onBlur) {
|
---|
| 206 | maybeOnBlur = {onBlur};
|
---|
| 207 | } else {
|
---|
| 208 | maybeOnBlur = {};
|
---|
| 209 | }
|
---|
| 210 |
|
---|
| 211 | const maybeRef = inputRef ? {ref: inputRef} : {};
|
---|
| 212 |
|
---|
| 213 | return React.createElement(element, {
|
---|
| 214 | ...props,
|
---|
| 215 | onChange: this.onChange,
|
---|
| 216 | value,
|
---|
| 217 | ...maybeOnKeyDown,
|
---|
| 218 | ...maybeOnBlur,
|
---|
| 219 | ...maybeRef
|
---|
| 220 | });
|
---|
| 221 | }
|
---|
| 222 | }
|
---|