Home Reference Source

src/components/controls/N2OSelect/N2OSelect.jsx

import React from 'react';
import PropTypes from 'prop-types';
import onClickOutside from 'react-onclickoutside';
import { isEqual, isEmpty } from 'lodash';
import { Button } from 'reactstrap';
import Popup from '../InputSelect/Popup';
import PopupList from '../InputSelect/PopupList';
import InputSelectGroup from '../InputSelect/InputSelectGroup';
import N2OSelectInput from './N2OSelectInput';

/**
 * N2OSelect
 * @reactProps {boolean} loading - флаг анимации загрузки
 * @reactProps {array} options - данные
 * @reactProps {string} valueFieldId - значение ключа value в данных
 * @reactProps {string} labelFieldId - значение ключа label в данных
 * @reactProps {string} iconFieldId - поле для иконки
 * @reactProps {string} imageFieldId - поле для картинки
 * @reactProps {string} badgeFieldId - поле для баджей
 * @reactProps {string} badgeColorFieldId - поле для цвета баджа
 * @reactProps {boolean} disabled - флаг неактивности
 * @reactProps {array} disabledValues - неактивные данные
 * @reactProps {string} filter - варианты фильтрации
 * @reactProps {string} value - текущее значение
 * @reactProps {function} onInput - callback при вводе в инпут
 * @reactProps {function} onChange - callback при выборе значения или вводе
 * @reactProps {function} onScrollEnd - callback при прокрутке скролла popup
 * @reactProps {string} placeHolder - подсказка в инпуте
 * @reactProps {boolean} resetOnBlur - фича, при которой сбрасывается значение контрола, если оно не выбрано из popup
 * @reactProps {function} onOpen - callback на открытие попапа
 * @reactProps {function} onClose - callback на закрытие попапа
 * @reactProps {string} groupFieldId - поле для группировки
 * @reactProps {boolean} closePopupOnSelect - флаг закрытия попапа при выборе
 * @reactProps {boolean} hasCheckboxes - флаг наличия чекбоксов
 * @reactProps {string} format - формат
 * @reactProps {boolean} searchByTap - поиск по нажатию кнопки
 */

class N2OSelect extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      value: '',
      isExpanded: false,
      options: this.props.options,
      selected: this.props.value ? [this.props.value] : [],
    };

    this._handleButtonClick = this._handleButtonClick.bind(this);
    this._handleInputChange = this._handleInputChange.bind(this);
    this._handleInputFocus = this._handleInputFocus.bind(this);
    this._hideOptionsList = this._hideOptionsList.bind(this);
    this._handleItemSelect = this._handleItemSelect.bind(this);
    this._removeSelectedItem = this._removeSelectedItem.bind(this);
    this._clearSelected = this._clearSelected.bind(this);
    this._handleSearchButton = this._handleSearchButton.bind(this);
    this._handleOnBlur = this._handleOnBlur.bind(this);
  }

  componentWillReceiveProps(nextProps) {
    let selected = [];
    if (!isEqual(nextProps.value, this.props.value)) {
      if (nextProps.value) {
        selected = [nextProps.value];
      } else {
        selected = [];
      }
    } else {
      selected = this.state.selected;
    }

    this.setState({
      options: nextProps.options,
      selected,
    });
  }

  /**
   * Удаляет элемент из списка выбранных
   * @param item - элемент
   * @private
   */

  _removeSelectedItem(item) {
    const { valueFieldId } = this.props;
    this.setState({
      selected: this.state.selected.filter(
        i => i[valueFieldId] !== item[valueFieldId]
      ),
    });
  }

  /**
   * Изменение видимости попапа
   * @param newState - новое значение видимости
   * @private
   */

  _changePopUpVision(newIsExpanded) {
    const { onOpen, onClose } = this.props;
    const { isExpanded } = this.state;
    if (isExpanded === newIsExpanded) return;
    this.setState(
      {
        isExpanded: newIsExpanded,
      },
      newIsExpanded ? onOpen : onClose
    );
  }

  /**
   * Обрабатывает нажатие на кнопку
   * @private
   */

  _handleButtonClick() {
    if (!this.props.disabled) {
      this._changePopUpVision(!this.state.isExpanded);
    }
  }

  /**
   * Обрабатывает форкус на инпуте
   * @private
   */

  _handleInputFocus() {
    this._changePopUpVision(true);
  }

  /**
   * Скрывает popUp
   * @private
   */

  _hideOptionsList() {
    this._changePopUpVision(false);
  }

  /**
   * Уставнавливает новое значение инпута
   * @param newValue - новое значение
   * @private
   */

  _setNewValue(newValue) {
    this.setState({
      value: newValue,
    });
  }

  /**
   * Удаляет выбранные элементы
   * @private
   */

  _clearSelected() {
    if (!this.props.disabled) {
      this.setState({
        selected: [],
      });
      this.props.onChange(null);
    }
  }

  /**
   * Выполняет поиск элементов для popUp, если установлен фильтр
   * @param newValue - значение для поиска
   * @private
   */

  _handleDataSearch(input, delay = true, callback) {
    const { onSearch, filter, options: data, labelFieldId } = this.props;
    if (filter) {
      const filterFunc = item =>
        String.prototype[this.props.filter].call(item, input);
      const options = data.filter(item =>
        filterFunc(item[labelFieldId].toString())
      );
      this.setState({ options: options });
    } else {
      onSearch(input, delay, callback);
    }
  }

  /**
   * Устанавливает выбранный элемент
   * @param item - элемент массива options
   * @private
   */

  _insertSelected(item) {
    this.setState({
      selected: [item],
    });
  }

  /**
   * Обрабатывает изменение инпута
   * @param newValue - новое значение
   * @private
   */

  _handleInputChange(newValue) {
    const { searchByTap, onChange, onInput, resetOnBlur } = this.props;

    this._setNewValue(newValue);

    !searchByTap && this._handleDataSearch(newValue);
    !resetOnBlur && onChange(newValue);
    onInput(newValue);
  }

  /**
   * Обрабатывает поиск по нажатию
   * @private
   */

  _handleSearchButton() {
    this._handleDataSearch(this.state.value);
  }

  /**
   * Очищает инпут и результаты поиска
   * @private
   */

  _clearSearchField() {
    this.setState({
      value: '',
      options: this.props.options,
    });
  }

  /**
   * Обрабатывает выбор элемента из popUp
   * @param item - элемент массива options
   * @private
   */

  _handleItemSelect(item) {
    this._insertSelected(item);

    if (this.props.closePopupOnSelect) {
      this._hideOptionsList();
    }

    this._clearSearchField();

    if (this.props.onChange) {
      this.props.onChange(item);
    }
  }

  /**
   * Обрабатывает поведение инпута при потери фокуса, если есть resetOnBlur
   * @private
   */

  _handleResetOnBlur() {
    if (this.props.resetOnBlur && !this.state.selected) {
      this.setState({
        value: '',
        options: this.props.options,
      });
    }
  }

  /**
   * Обрабатывает клик за пределы компонента
   * @param evt
   */

  handleClickOutside(evt) {
    this._hideOptionsList();
    this._handleResetOnBlur();
  }

  _handleOnBlur(e) {
    e.preventDefault();
    this._handleResetOnBlur();
    this.props.onBlur();
  }

  /**
   * Рендер
   */

  render() {
    const {
      loading,
      className,
      valueFieldId,
      labelFieldId,
      iconFieldId,
      disabled,
      disabledValues,
      imageFieldId,
      groupFieldId,
      format,
      placeholder,
      badgeFieldId,
      badgeColorFieldId,
      onScrollEnd,
      hasSearch,
      cleanable,
      style,
    } = this.props;
    const inputSelectStyle = { width: '100%', ...style };

    const { selected } = this.state;

    return (
      <div
        className="n2o-input-select"
        style={inputSelectStyle}
        onBlur={this._handleOnBlur}
      >
        <Button>
          <InputSelectGroup
            className={className}
            isExpanded={this.state.isExpanded}
            loading={loading}
            disabled={disabled}
            onButtonClick={this._handleButtonClick}
            iconFieldId={iconFieldId}
            imageFieldId={imageFieldId}
            cleanable={cleanable}
            selected={this.state.selected}
            onClearClick={this._clearSelected}
          >
            {!isEmpty(selected) && selected[0][labelFieldId]}
          </InputSelectGroup>
        </Button>
        <Popup isExpanded={this.state.isExpanded}>
          <React.Fragment>
            {hasSearch && (
              <N2OSelectInput
                placeholder={placeholder}
                onChange={this._handleInputChange}
                onSearch={this._handleSearchButton}
                value={this.state.value}
              />
            )}
            <PopupList
              options={this.state.options}
              valueFieldId={valueFieldId}
              labelFieldId={labelFieldId}
              iconFieldId={iconFieldId}
              badgeFieldId={badgeFieldId}
              badgeColorFieldId={badgeColorFieldId}
              onSelect={this._handleItemSelect}
              onScrollEnd={onScrollEnd}
              isExpanded={this.state.isExpanded}
              selected={this.state.selected}
              disabledValues={disabledValues}
              groupFieldId={groupFieldId}
              hasCheckboxes={false}
              onRemoveItem={this._removeSelectedItem}
              format={format}
            />
          </React.Fragment>
        </Popup>
      </div>
    );
  }
}

N2OSelect.propTypes = {
  loading: PropTypes.bool,
  options: PropTypes.array.isRequired,
  valueFieldId: PropTypes.string.isRequired,
  labelFieldId: PropTypes.string.isRequired,
  cleanable: PropTypes.bool,
  iconFieldId: PropTypes.string,
  imageFieldId: PropTypes.string,
  badgeFieldId: PropTypes.string,
  badgeColorFieldId: PropTypes.string,
  disabled: PropTypes.bool,
  disabledValues: PropTypes.array,
  filter: PropTypes.oneOf(['includes', 'startsWith', 'endsWith', false]),
  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  onInput: PropTypes.func,
  onChange: PropTypes.func,
  onScrollEnd: PropTypes.func,
  placeholder: PropTypes.string,
  resetOnBlur: PropTypes.bool,
  onOpen: PropTypes.func,
  onClose: PropTypes.func,
  groupFieldId: PropTypes.string,
  format: PropTypes.string,
  searchByTap: PropTypes.bool,
  onSearch: PropTypes.func,
  hasSearch: PropTypes.func,
};

N2OSelect.defaultProps = {
  parentFieldId: 'parentId',
  cleanable: true,
  valueFieldId: 'id',
  labelFieldId: 'name',
  iconFieldId: 'icon',
  imageFieldId: 'image',
  badgeFieldId: 'badge',
  loading: false,
  disabled: false,
  disabledValues: [],
  resetOnBlur: false,
  filter: false,
  searchByTap: false,
  hasSearch: false,
  onSearch() {},
  onChange() {},
  onScrollEnd() {},
  onInput() {},
  onOpen() {},
  onClose() {},
  onBlur() {},
};

export default onClickOutside(N2OSelect);