【React】自定义带历史记录的搜索框

278 阅读1分钟
/* eslint-disable @typescript-eslint/no-unused-vars */
// SearchBox/index.jsx
/**
 * @desc 自定义带历史记录的搜索框
 * @author wangjun
 * @date 2021-07-23
 */
import React, { Component } from 'react';
import { Input, Button } from 'antd';
import styles from './index.less';

const ADD_DELAY = 500;

export default class SearchBox extends Component {
  /**
   * @prop {string?} value 绑定的值,如果传入值,则视为完成受控
   * @prop {string[]?} defaultOptions 默认历史搜索记录
   * @prop {function?} onChange 输入内容|选择历史记录项时触发
   * @prop {function?} onSearch 按下回车|点击搜索按钮|选择历史记录项时触发
   * @prop {function?} onOptionsUpdate 历史记录列表更新时触发
   *
   * @prop {number?} maxLength 文本框最大内容长度
   * @prop {string?} placeholder 提示文本
   * @prop {boolean?} allowClear 是否添加清空按钮
   *
   * @prop {string?} width 容器宽度
   * @prop {number?} zIndex 历史记录下拉组件的层级
   * @prop {number?} maxOptionsLength 最大历史记录数量
   * @prop {boolean?} loading 是否在加载中(防止高频操作)
   * @prop {string?} enterButton 按钮文字
   * @prop {object?} style style
   */
  static defaultProps = {
    value: undefined,
    defaultOptions: [],
    onChange: (value = '') => {},
    onSearch: (value = '') => {},
    onOptionsUpdate: (options = []) => {},

    maxLength: 30,
    placeholder: '输入关键字',
    allowClear: false,

    //  width: "300px",
    zIndex: 10,
    maxOptionsLength: 10,
    loading: false,
    enterButton: '查询',
  };

  state = {
    controlled: this.props.value !== undefined && typeof this.props.value === 'string',

    value: '',
    options: this.props.defaultOptions.slice(),
    visibleOptions: false,
  };

  refInput = React.createRef();

  onInputChange = (event = {}) => {
    const value = event.target.value;
    if (this.state.controlled) {
      this.props.onChange && this.props.onChange(value);
    } else {
      this.setState({ value });
    }
  };

  onHitEnter = (event = {}) => {
    const { keyCode, which } = event;

    if (keyCode === 13 || which === 13) {
      this.addOptions(this.useValue);
      this.props.onSearch && this.props.onSearch(this.useValue);
      this.refInput.current.blur();
    }
  };

  onSearchButtonClick = () => {
    this.addOptions(this.useValue);
    if (this.props.onSearch) {
      this.props.onSearch(this.useValue);
    }
  };

  addOptions = (value = '') => {
    if (!value.trim()) {
      return;
    }

    let { options = [] } = this.state;
    const included = options.some(v => v === value);
    if (!included) {
      const t = setTimeout(() => {
        clearTimeout(t);
        options.unshift(value);
        options = options.slice(0, Math.max(3, this.props.maxOptionsLength));
        this.setState({ options }, () => {
          if (this.props.onOptionsUpdate) {
            this.props.onOptionsUpdate(options);
          }
        });
      }, ADD_DELAY);
    }
  };

  onOptionClick = (value = '') => {
    if (this.state.controlled) {
      this.props.onChange && this.props.onChange(value);
    } else {
      this.setState({ value });
    }
    this.props.onSearch && this.props.onSearch(value);
  };

  get useValue() {
    return this.state.controlled ? this.props.value : this.state.value;
  }

  render() {
    const { maxLength, placeholder, allowClear, style, zIndex, loading, enterButton } = this.props;
    const { options = [], visibleOptions } = this.state;

    return (
      <div className={styles.container} style={{ ...style, zIndex }}>
        <Input
          ref={this.refInput}
          className={styles.input}
          type="text"
          maxLength={maxLength}
          placeholder={placeholder}
          allowClear={allowClear}
          value={this.useValue}
          onChange={this.onInputChange}
          onKeyUp={this.onHitEnter}
          onFocus={() => this.setState({ visibleOptions: true })}
          onBlur={() => this.setState({ visibleOptions: false })}
        />
        <Button
          loading={loading}
          type="primary"
          className={styles.search_btn}
          onClick={this.onSearchButtonClick}
        >
          {enterButton}
        </Button>

        <div className={loading ? styles.loading_active : styles.loading}></div>

        <ul
          className={options.length > 0 && visibleOptions ? styles.options_visible : styles.options}
        >
          {options.map(opt => (
            <li
              key={opt}
              className={styles.option_item}
              onClick={() => this.onOptionClick(opt)}
              title={opt}
            >
              {opt}
            </li>
          ))}
        </ul>
      </div>
    );
  }
}