Home Reference Source

src/components/controls/InputSelectTreeOldDeprecated/InputSelectTree.jsx

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import onClickOutside from 'react-onclickoutside';
import { Dropdown, DropdownToggle } from 'reactstrap';
import InputSelectGroup from '../InputSelect/InputSelectGroup';
import InputContent from '../InputSelect/InputContent';
import Popup from '../InputSelect/Popup';
import TreeItems from './TreeItems';
import { isEqual, get, unionBy, find, merge, isNil } from 'lodash';
import NODE_SELECTED from './nodeSelected.js';
import { SHOW_CHILD } from 'rc-tree-select';

/**
 * InputSelectTree
 * @reactProps {boolean} hasChildrenFieldId
 * @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 {function} onSelect
 * @reactProps {boolean} multiSelect - флаг мульти выбора
 * @reactProps {string} groupFieldId - поле для группировки
 * @reactProps {boolean} closePopupOnSelect - флаг закрытия попапа при выборе
 * @reactProps {boolean} hasCheckboxes - флаг наличия чекбоксов
 * @reactProps {string} format - формат
 * @reactProps {boolean} collapseSelected - флаг сжатия выбранных элементов
 * @reactProps {number} lengthToGroup - от скольки элементов сжимать выбранные элементы
 * @reactProps {boolean} ajax - загрузка элементов при раскрытии
 */

class InputSelectTree extends React.Component {
  constructor(props) {
    super(props);
    const {
      value,
      options,
      valueFieldId,
      labelFieldId,
      multiSelect,
    } = this.props;
    const valueArray = Array.isArray(value) ? value : value ? [value] : [];
    const input = value && !multiSelect ? value[labelFieldId] : '';
    this.state = {
      isExpanded: false,
      value: value || [],
      selected: [],
      options,
      active: null,
      treeStates: this.getTreeItems(this.props.options),
      input,
    };

    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._handleScrollEnd = this._handleScrollEnd.bind(this);
    this._removeSelectedItem = this._removeSelectedItem.bind(this);
    this._clearSelected = this._clearSelected.bind(this);
    this._handleElementCreate = this._handleElementCreate.bind(this);
    this._handleElementClear = this._handleElementClear.bind(this);
    this.onExpandClick = this.onExpandClick.bind(this);
    this._handleRemove = this._handleRemove.bind(this);
    this.handleKeyPress = this.handleKeyPress.bind(this);
    this.handleFocus = this.handleFocus.bind(this);
    this._handleResetOnBlur = this._handleResetOnBlur.bind(this);
    this._toggle = this._toggle.bind(this);
    this._handleArrowDown = this._handleArrowDown.bind(this);
    this._handleArrowUp = this._handleArrowUp.bind(this);
    this._handleArrowRight = this._handleArrowRight.bind(this);
    this._handleArrowLeft = this._handleArrowLeft.bind(this);
    this._handleEnter = this._handleEnter.bind(this);
    this._setIsExpanded = this._setIsExpanded.bind(this);
    this._handleClick = this._handleClick.bind(this);
    this._clearSelected = this._clearSelected.bind(this);
    this._setNewInputValue = this._setNewInputValue.bind(this);
    this._setInputFocus = this._setInputFocus.bind(this);

    this.KEY_MAPPER = {
      ArrowDown: this._handleArrowDown,
      ArrowUp: this._handleArrowUp,
      ArrowRight: this._handleArrowRight,
      ArrowLeft: this._handleArrowLeft,
      Enter: this._handleEnter,
    };
  }

  componentWillReceiveProps(nextProps) {
    const {
      multiSelect,
      value,
      valueFieldId,
      labelFieldId,
      options,
      loading,
    } = nextProps;
    if (!isEqual(nextProps.options, this.state.options)) {
      this.setState({ options, treeStates: this.getTreeItems(options) });
    }
    if (!isEqual(nextProps.value, this.props.value)) {
      const valueArray = Array.isArray(value) ? value : value ? [value] : [];
      const input = value && !multiSelect ? value[labelFieldId] : '';

      this.setState({ value: valueArray, input });
    }
  }

  _handleChange() {
    const { selected } = this.state;
    const { onChange, multiSelect } = this.props;

    if (!onChange) {
      return false;
    }

    if (selected.length) {
      multiSelect ? onChange(selected) : onChange(selected[0]);
    } else {
      onChange(null);
    }
  }

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

  _handleRemove(item) {
    let items = [];

    if (this.props.multiSelect) {
      items = [...this._retrieveChilds(item), item];
      this.changeSelected(items, NODE_SELECTED.NOT_SELECTED);
      this.unselectParent(item);
    } else {
      items.push(item);
    }

    items.map(node => this._removeSelectedItem(node));
  }

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

  // _removeSelectedItem(item) {
  //   this.setState(
  //     prevState => ({ selected: prevState.selected.filter(i => i.id !== item.id) }),
  //     () => this._unselectNode(item)
  //   );
  // }

  _removeSelectedItem(item) {
    const { onChange } = this.props;
    const value = this.state.value.filter(i => i.id !== item.id);
    this.setState({ value }, onChange(this._getValue()));
  }

  /**
   * Обрабатывает конец скролла popUp
   * @private
   */

  _handleScrollEnd() {
    if (this.props.onScrollEnd) {
      this.props.onScrollEnd();
    }
  }

  /**
   * новое значение инпута search)
   * @param input
   * @private
   */
  _setNewInputValue(input) {
    const { onInput, resetOnBlur, multiSelect } = this.props;
    const { value } = this.state;
    const onSetNewInputValue = input => {
      onInput(input);
      this._handleDataSearch(input);
    };

    if (this.state.input !== input) {
      this._setSelected(false);
      this.setState({ input }, () => onSetNewInputValue(input));
    }
  }

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

  _changePopUpVision(newState) {
    if (this.state.isExpanded === newState) {
      return;
    }

    if (this.state.isExpanded && this.props.onClose) {
      this.props.onClose();
    }

    this.setState({
      isExpanded: newState,
    });
  }

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

  _handleButtonClick() {
    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,
    });
  }

  /**
   * Выполняет поиск по дереву
   * @param options - элементы по которым искать
   * @param searchValue - поисковый запрос
   * @returns {*}
   * @private
   */

  _makeSearch(options, searchValue) {
    const filter = item =>
      String.prototype[this.props.filter].call(item, searchValue);

    const { valueFieldId, parentFieldId } = this.props;

    return options.filter(item => {
      const childs = this.state.options.filter(
        node => node[parentFieldId] === item[valueFieldId]
      );

      return (
        filter(item[this.props.labelFieldId].toString()) ||
        this._makeSearch(childs, searchValue).length > 0
      );
    });
  }

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

  _handleDataSearch(input, delay = true, callback) {
    const { onSearch, filter, labelFieldId, options } = this.props;
    if (filter && ['includes', 'startsWith', 'endsWith'].includes(filter)) {
      const filterFunc = item => String.prototype[filter].call(item, input);
      const filteredData = options.filter(item =>
        filterFunc(item[labelFieldId])
      );
      this.setState({ options: filteredData });
    } else {
      //серверная фильтрация
      const labels = this.state.value.map(item => item[labelFieldId]);
      if (labels.some(label => label === input)) {
        onSearch('', delay, callback);
      } else {
        onSearch(input, delay, callback);
      }
    }
  }

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

  _insertSelected(items) {
    const { valueFieldId } = this.props;

    if (this.props.multiSelect) {
      this.setState(prevState => ({
        selected: unionBy(prevState.selected, items, valueFieldId),
      }));
    } else {
      this.setState({
        selected: items,
      });
    }
  }

  /**
   * Обрабатывает выбор элемента
   * @param item
   * @private
   */

  _handleSelect(item) {
    const { multiSelect } = this.props;
    let items = [];

    if (multiSelect) {
      items = [item, ...this._retrieveChilds(item)];
      this.selectParent(item);
    } else {
      items.push(item);
      this._unselectAll();
    }

    this._insertSelected(items);
    this.changeSelected(items, NODE_SELECTED.SELECTED);
  }

  /**
   * Изменяет стейт выбора всех элементов
   * @private
   */

  _unselectAll() {
    const options = this.state.treeStates;

    Object.entries(options).forEach(([_, item]) => {
      if (item.selected === NODE_SELECTED.SELECTED) {
        item.selected = NODE_SELECTED.NOT_SELECTED;
      }
    });

    this.setState({ treeStates: options });
  }

  /**
   * Изменяет стейт выбора для одного элемент
   * @param node - элемент дерева
   * @private
   */

  _unselectNode(node) {
    this.setState(prevState => {
      const { treeStates } = prevState;
      const { valueFieldId } = this.props;
      treeStates[node[valueFieldId]].selected = NODE_SELECTED.NOT_SELECTED;

      return { treeStates };
    });
  }

  /**
   * Ищет все элементы поддерева
   * @param item - корень поддерева
   * @returns {*[]}
   * @private
   */

  _retrieveChilds(item) {
    const { valueFieldId, parentFieldId } = this.props;
    const childs = this.state.options.filter(
      node => node[parentFieldId] === item[valueFieldId]
    );

    childs.length > 0 &&
      childs.map(child => {
        childs.concat(this._retrieveChilds(child));
      });

    return childs;
  }

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

  _removeChild(item) {
    const { valueFieldId, parentFieldId } = this.props;
    if (this.props.multiSelect) {
      const childs = this.state.options.filter(
        node => node[parentFieldId] === item[valueFieldId]
      );

      childs.length > 0 &&
        childs.map(child => {
          this._removeSelectedItem(child);
          this._removeChild(child);
        });
    }
  }

  /**
   * Очищает выбранный элемент
   * @private
   */

  _clearSelected() {
    const { onChange } = this.props;
    this.setState({ value: [], input: '' }, () => onChange(this._getValue()));
  }

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

  _handleInputChange(newValue) {
    this._setNewValue(newValue);
    this._handleDataSearch(newValue);

    // if (!this.props.multiSelect) {
    //   this._clearSelected();
    // }

    if (!this.props.resetOnBlur && this.props.onChange) {
      this.props.onChange(newValue);
    }

    if (this.props.onInput) {
      this.props.onInput(newValue);
    }
  }

  /**
   * Возвращает текущее значение (массив - если ипут селект, объект - если нет)
   * или null если пусто
   * @returns {*}
   * @private
   */
  _getValue() {
    const { multiSelect } = this.props;
    const { value } = this.state;
    const rObj = multiSelect ? value : value[0];
    return rObj || null;
  }

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

  _handleItemSelect(item) {
    const {
      multiSelect,
      closePopupOnSelect,
      labelFieldId,
      options,
      onSelect,
      onChange,
    } = this.props;
    const selectCallback = () => {
      closePopupOnSelect && this._hideOptionsList();
      onSelect(item);
      onChange(this._getValue());
      this._setSelected(true);
    };

    this.setState(
      prevState => ({
        value: multiSelect ? [...prevState.value, item] : [item],
        input: multiSelect ? '' : item[labelFieldId],
        options,
      }),
      selectCallback
    );
  }

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

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

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

  _handleResetOnBlur() {
    const { value, input, options } = this.state;
    const { onChange, multiSelect, resetOnBlur, labelFieldId } = this.props;
    const newValue = find(options, { [labelFieldId]: input });
    if (!newValue) {
      if (!resetOnBlur) {
        if (input) {
          const createdValue = { [labelFieldId]: input };
          this.setState(
            {
              value: multiSelect ? [...value, createdValue] : [createdValue],
              input: multiSelect ? '' : input,
            },
            () => onChange(this._getValue())
          );
          onChange(multiSelect ? [...value, createdValue] : createdValue);
        }
      } else {
        if (input) {
          this.setState(
            {
              input: multiSelect
                ? ''
                : (value[0] && value[0][labelFieldId]) || '',
              value,
            },
            () => onChange(this._getValue())
          );
        } else {
          this.setState(
            {
              input: '',
              value: multiSelect ? value : [],
            },
            () => onChange(this._getValue())
          );
        }
      }
    } else {
      this.setState(
        {
          value: multiSelect ? [...value, newValue] : [newValue],
          input: multiSelect ? '' : input,
        },
        () => onChange(this._getValue())
      );
    }
  }

  _handleElementClear() {
    this._clearSearchField();
    this._clearSelected();
  }

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

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

  /**
   * Обрабатывает создание элемента
   * @private
   */

  _handleElementCreate() {
    const { valueFieldId } = this.props;
    const newElement = {
      [this.props.labelFieldId]: this.state.value,
    };

    if (this.props.onElementCreate) {
      this.props.onElementCreate(newElement);
    }

    newElement[valueFieldId] = `${newElement.value}_${new Date().getTime()}`;

    this.setState({
      selected: [...this.state.selected, newElement],
    });

    this._setNewValue('');
  }

  inArray(array, item) {
    const { valueFieldId } = this.props;
    return (
      array.filter(
        disabledItem => disabledItem[valueFieldId] === item[valueFieldId]
      ).length > 0
    );
  }

  /**
   * Обработчик нажатия на кнопку раскрытия элемента
   * @param item
   */

  onExpandClick(item) {
    const { valueFieldId } = this.props;
    const itemState = this.state.treeStates[item[valueFieldId]];
    this._handleExpandClick(itemState);
  }

  /**
   * Получает state для новых элементов дерева
   * @param options
   */

  getTreeItems(options, mergeData = null) {
    const result = {};
    const { valueFieldId, parentFieldId, hasChildrenFieldId } = this.props;
    const treeStates = get(this, 'state.treeStates', false);

    options.map(
      item =>
        (result[item[valueFieldId]] = {
          [valueFieldId]: item[valueFieldId],
          parentId: item[parentFieldId],
          expanded: false,
          loaded: false,
          selected: NODE_SELECTED.NOT_SELECTED,
          ref: React.createRef(),
          [hasChildrenFieldId]: isNil(item[hasChildrenFieldId])
            ? true
            : item[hasChildrenFieldId],
        })
    );

    return treeStates ? merge(result, treeStates) : result;
  }

  /**
   * Добавляет новым элементам дерева их state
   * @param elements - новые элементы
   */

  addNewElements(elements) {
    const { valueFieldId } = this.props;
    const newElements = elements.filter(
      item => !this.state.treeStates.hasOwnProperty(item[valueFieldId])
    );

    this.setState(prevState => ({
      treeStates: {
        ...prevState.treeStates,
        ...this.getTreeItems(newElements),
      },
    }));
  }

  /**
   * Изменяет стейт элемента дерева
   * @param itemState - стейс элемента
   * @param id - id элемента
   */

  overrideElement(itemState, id) {
    const newData = Object.assign({
      ...this.state.treeStates,
      [id]: itemState,
    });

    this.setState({ treeStates: newData });
  }

  /**
   * Изменения статус выбора у заданных элементов
   * @param nodes - элементы дерева
   * @param newStatus - новый статус
   */

  changeSelected(nodes, newStatus) {
    const { valueFieldId } = this.props;
    nodes.map(node => {
      const nodeState = this.state.treeStates[node[valueFieldId]];

      nodeState.selected = newStatus;
      this.overrideElement(nodeState);
    });
  }

  /**
   * Устанавливает статус выбора у родительских элементов дерева
   * @param item - элемент дерева
   */

  selectParent(item) {
    const { valueFieldId, parentFieldId } = this.props;
    const parent = this.state.options[
      this.state.options.findIndex(
        parent => parent[valueFieldId] === item[parentFieldId]
      )
    ];

    if (!parent) {
      return;
    }

    const parentState = this.state.treeStates[parent[valueFieldId]];

    if (parentState.selected === NODE_SELECTED.NOT_SELECTED) {
      parentState.selected = NODE_SELECTED.PARTIALLY;
      this.overrideElement(parentState, parent[valueFieldId]);

      if (parent[parentFieldId]) {
        this.selectParent(parent);
      }
    }
  }

  /**
   * Снимает статус выбора с родительских элементов дерева
   * @param item - элемент дерева
   */

  unselectParent(item) {
    const { valueFieldId, parentFieldId } = this.props;
    const parent = this.state.options[
      this.state.options.findIndex(
        parent => parent[valueFieldId] === item[parentFieldId]
      )
    ];
    const parentState = parent
      ? this.state.treeStates[parent[valueFieldId]]
      : null;

    if (parentState && parentState.selected === NODE_SELECTED.PARTIALLY) {
      const selectedChild = this.state.options.find(
        child =>
          child[parentFieldId] === parent[valueFieldId] &&
          this.state.treeStates[child[valueFieldId]].selected ===
            NODE_SELECTED.SELECTED
      );

      if (!selectedChild) {
        parentState.selected = NODE_SELECTED.NOT_SELECTED;
        this.overrideElement(parentState, parent[valueFieldId]);
      }

      if (parent[parentFieldId]) {
        this.unselectParent(parent);
      }
    }
  }

  /**
   * Обрабатывает нажатия на кнопку клавиатуры "Стрелка вниз"
   * @private
   */

  _handleArrowDown() {
    if (this.state.active) {
      this._setNextNodeActive();
    } else {
      this._setFirstNodeActive();
    }
  }

  /**
   * Обрабатывает нажатия на кнопку клавиатуры "Стрелка вверх"
   * @private
   */

  _handleArrowUp() {
    if (this.state.active) {
      this._setPrevNodeActive();
    }
  }

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

  _handleArrowRight() {
    if (this.state.active) {
      if (!this.state.active.expanded && this.state.active.hasChildren) {
        this._handleExpandClick(this.state.active, true);
      } else {
        this._setNextNodeActive();
      }
    } else {
      this._setFirstNodeActive();
    }
  }

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

  _handleArrowLeft() {
    const { active } = this.state;
    const { parentFieldId } = this.props;

    if (active) {
      if (active.expanded) {
        this._collapseNode();
      } else if (active[parentFieldId]) {
        this.setState({ active: this.state.treeStates[active[parentFieldId]] });
        this._scrollOntoElement(this.state.treeStates[active[parentFieldId]]);
      } else {
        this._setPrevNodeActive();
      }
    }
  }

  /**
   * Схлопывает элемент дерева
   * @private
   */

  _collapseNode() {
    const { valueFieldId } = this.props;
    const activeItem = {
      ...this.state.active,
      expanded: false,
    };

    this.overrideElement(activeItem, activeItem[valueFieldId]);
    this.setState({ active: activeItem });
  }

  /**
   * Устанавливает первый элемент дерева активным
   * @private
   */

  _setFirstNodeActive() {
    const { valueFieldId, parentFieldId } = this.props;
    const rootNodes = this.state.options.filter(node => !node[parentFieldId]);
    this.setState({
      active: this.state.treeStates[rootNodes[0][valueFieldId]],
    });
  }

  /**
   * Устанавливает следующий элемент дерева активный
   * @private
   */

  _setNextNodeActive() {
    const { valueFieldId } = this.props;
    const nextItem = this.getNextItem(this.state.active);

    if (nextItem) {
      const nextItemState = this.state.treeStates[nextItem[valueFieldId]];
      this.setState({ active: nextItemState });
      this._scrollOntoElement(nextItemState);
    }
  }

  /**
   * Устанавливает предыдущий элемент дерева активный
   * @private
   */

  _setPrevNodeActive() {
    const { valueFieldId } = this.props;
    const prevItem = this.getPrevItem(this.state.active);

    if (prevItem) {
      const prevItemState = this.state.treeStates[prevItem[valueFieldId]];
      this.setState({ active: prevItemState });
      this._scrollOntoElement(prevItemState);
    }
  }

  _scrollOntoElement(nodeState) {
    const nodeElement = ReactDOM.findDOMNode(nodeState.ref.current);
    nodeElement.scrollIntoView();
  }

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

  _handleEnter() {
    if (this.state.active) {
      this._handleItemSelect(this.state.active);
    }
  }

  /**
   * Обрабатывает нажатия на кнопку клавиатуры
   * @param e - событие нажатия
   */

  handleKeyPress(e) {
    if (this.KEY_MAPPER.hasOwnProperty(e.key)) {
      this.KEY_MAPPER[e.key]();
    }
  }

  /**
   * Обрабатывает изменение активного элемента
   * @param item
   */

  handleFocus(item) {
    this.setState({ active: item });
  }

  /**
   * Находит предыдущий элемент
   * @param currentItem - текущий элемент
   * @returns {*}
   */

  getPrevItem(currentItem) {
    const { parentFieldId } = this.props;
    if (currentItem[parentFieldId]) {
      return this._findPrevParentItem(currentItem);
    }
    return this._findPrevRootItem(currentItem);
  }

  /**
   * Находит предыдущий элемент без родителя
   * @param currentItem - текущий активный элемент
   * @returns {*}
   * @private
   */

  _findPrevRootItem(currentItem) {
    const { valueFieldId, parentFieldId } = this.props;
    const rootNodes = this.state.options.filter(node => !node[parentFieldId]);
    const prevNode = this._findPrevItem(rootNodes, currentItem);

    if (!prevNode) {
      this._changePopUpVision(false);
      return null;
    } else if (!this.state.treeStates[prevNode[valueFieldId]].expanded) {
      return prevNode;
    }
    return this._getLowest(prevNode);
  }

  /**
   * Находит предыдущий элемент-родитель
   * @param currentItem - текущий элемент
   * @returns {*}
   * @private
   */

  _findPrevParentItem(currentItem) {
    const { valueFieldId, parentFieldId } = this.props;
    const sameLevels = this.state.options.filter(
      node => node[parentFieldId] === currentItem[parentFieldId]
    );
    const nextItem = this._findPrevItem(sameLevels, this.state.active);

    if (nextItem) {
      return nextItem;
    }
    return this.state.options[
      this.state.options.findIndex(
        node => node[valueFieldId] === currentItem[parentFieldId]
      )
    ];
  }

  /**
   * Находит самый последний элемент в поддереве
   * @param item - корень поддерева
   * @returns {*}
   * @private
   */

  _getLowest(item) {
    const { valueFieldId, parentFieldId } = this.props;
    const childs = this.state.options.filter(
      node => node[parentFieldId] === item[valueFieldId]
    );
    const lastChild = childs[childs.length - 1];

    if (this.state.treeStates[lastChild[valueFieldId]].expanded) {
      return this._getLowest(lastChild);
    }
    return lastChild;
  }

  /**
   * Находит следующий элемент
   * @param currentItem
   * @param lookChilds
   * @returns {*}
   */

  getNextItem(currentItem, lookChilds = true) {
    const { valueFieldId, parentFieldId } = this.props;
    const childs = this.state.options.filter(
      node => node[parentFieldId] === currentItem[valueFieldId]
    );
    const itemState = this.state.treeStates[currentItem[valueFieldId]];

    if (lookChilds && itemState.expanded && childs.length > 0) {
      return childs[0];
    } else if (currentItem[parentFieldId]) {
      return this._findNextParentItem(currentItem);
    }
    const rootNodes = this.state.options.filter(node => !node[parentFieldId]);

    return this._findNextItem(rootNodes, currentItem);
  }

  /**
   * Находит следующий элемент-родителя
   * @param currentItem
   * @returns {*}
   * @private
   */

  _findNextParentItem(currentItem) {
    const { valueFieldId, parentFieldId } = this.props;
    const sameLevels = this.state.options.filter(
      node => node[parentFieldId] === currentItem[parentFieldId]
    );
    const nextItem = this._findNextItem(sameLevels, this.state.active);

    if (nextItem) {
      return nextItem;
    }
    const parent = this.state.options[
      this.state.options.findIndex(
        node => node[valueFieldId] === currentItem[parentFieldId]
      )
    ];

    return this.getNextItem(parent, false);
  }

  /**
   * скрыть / показать попап
   * @param isExpanded
   * @private
   */
  _setIsExpanded(isExpanded) {
    const { onToggle, onClose, onOpen } = this.props;
    const { isExpanded: previousIsExpanded } = this.state;
    if (isExpanded !== previousIsExpanded) {
      this.setState({ isExpanded });
      onToggle();
      isExpanded ? onOpen() : onClose();
    }
  }

  /**
   * Получает следующий по-порядку элемент из списка элементов
   * @param nodes - список элементов
   * @param active - активный элемент
   * @returns {null}
   * @private
   */

  _findNextItem(nodes, active) {
    const { valueFieldId } = this.props;
    const currentIndex = nodes.findIndex(
      node => node[valueFieldId] === active[valueFieldId]
    );
    const nextIndex = currentIndex + 1;

    return nextIndex <= nodes.length ? nodes[nextIndex] : null;
  }

  /**
   * Получает предыдущий по-порядку элемент из списка элементов
   * @param nodes - список элементов
   * @param active - активный элемент
   * @returns {null}
   * @private
   */

  _findPrevItem(nodes, active) {
    const { valueFieldId } = this.props;
    const currentIndex = nodes.findIndex(
      node => node[valueFieldId] === active[valueFieldId]
    );
    const nextIndex = currentIndex - 1;

    return nextIndex >= 0 ? nodes[nextIndex] : null;
  }

  /**
   * Обработчик раскрытия элемента дерева
   * @param itemState - стейт элемента для раскрытия
   * @param newState - новый статус
   * @private
   */

  async _handleExpandClick(itemState, newState = null) {
    const { valueFieldId } = this.props;
    if (this.props.ajax && !itemState.loaded) {
      itemState.expanded = true;
      itemState.loaded = true;
      this.props.handleItemOpen(itemState[valueFieldId]);
    } else {
      itemState.expanded = newState || !itemState.expanded;
    }

    this.overrideElement(itemState, itemState[valueFieldId]);
  }

  _handleClick() {
    // const searchCallback = () => {
    this._setIsExpanded(true);
    // };
    //this._handleDataSearch(this.state.input, false, searchCallback);
    this._setSelected(false);
    this._setInputFocus(true);
  }

  _toggle() {
    const { closePopupOnSelect } = this.props;
    this.setState(prevState => {
      if (!closePopupOnSelect && prevState.isExpanded) return false;

      return { isExpanded: !prevState.isExpanded };
    });
  }
  _setSelected(isInputSelected) {
    this.setState({ isInputSelected });
  }

  _setInputFocus(inputFocus) {
    this.setState({ inputFocus });
  }

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

  render() {
    const {
      loading,
      labelFieldId,
      iconFieldId,
      disabled,
      placeholder,
      multiSelect,
      disabledValues,
      imageFieldId,
      hasCheckboxes,
      format,
      ajax,
      lengthToGroup,
      collapseSelected,
      badgeFieldId,
      badgeColorFieldId,
      groupFieldId,
      valueFieldId,
      style,
      expandPopUp,
      parentFieldId,
      hasChildrenFieldId,
    } = this.props;
    const inputSelectStyle = { width: '100%', cursor: 'text', ...style };

    return (
      <Dropdown
        style={inputSelectStyle}
        className="n2o-input-select"
        toggle={() => {}}
        onBlur={() => {
          this._setInputFocus(false);
          this._setSelected(false);
        }}
        onFocus={() => {
          this._setInputFocus(true);
          this._setSelected(true);
        }}
        isOpen={this.state.isExpanded && !disabled}
      >
        <DropdownToggle disabled={disabled}>
          <InputSelectGroup
            isExpanded={this.state.isExpanded}
            setIsExpanded={this._setIsExpanded}
            loading={loading}
            selected={this.state.value}
            input={this.state.input}
            iconFieldId={iconFieldId}
            imageFieldId={imageFieldId}
            multiSelect={multiSelect}
            isInputInFocus={this.state.inputFocus}
            onClearClick={this._handleElementClear}
          >
            <InputContent
              loading={loading}
              value={this.state.input}
              disabledValues={disabledValues}
              valueFieldId={valueFieldId}
              placeholder={placeholder}
              options={this.state.options}
              openPopUp={() => this._setIsExpanded(true)}
              closePopUp={() => this._setIsExpanded(false)}
              onInputChange={this._setNewInputValue}
              onRemoveItem={this._removeSelectedItem}
              isExpanded={this.state.isExpanded}
              isSelected={this.state.isInputSelected}
              inputFocus={this.state.inputFocus}
              iconFieldId={iconFieldId}
              activeValueId={this.state.activeValueId}
              setActiveValueId={this._setActiveValueId}
              imageFieldId={imageFieldId}
              selected={this.state.value}
              labelFieldId={labelFieldId}
              clearSelected={this._clearSelected}
              multiSelect={multiSelect}
              onClick={this._handleClick}
              onSelect={this._handleItemSelect}
            />
          </InputSelectGroup>
        </DropdownToggle>
        <Popup isExpanded={this.state.isExpanded} expandPopUp={expandPopUp}>
          <TreeItems
            value={this.state.value}
            options={this.state.options}
            treeStates={this.state.treeStates}
            hasCheckboxes={hasCheckboxes}
            imageFieldId={imageFieldId}
            iconFieldId={iconFieldId}
            labelFieldId={labelFieldId}
            valueFieldId={valueFieldId}
            parentFieldId={parentFieldId}
            badgeFieldId={badgeFieldId}
            badgeColorFieldId={badgeColorFieldId}
            format={format}
            hasChildrenFieldId={hasChildrenFieldId}
            ajax={ajax}
            active={this.state.active}
            handleSelect={this._handleItemSelect}
            handleDelete={this._handleRemove}
            onExpandClick={this.onExpandClick}
            handleFocus={this.handleFocus}
            disabledValues={disabledValues}
            groupFieldId={groupFieldId}
          />
        </Popup>
      </Dropdown>
    );
  }
}

InputSelectTree.propTypes = {
  hasChildrenFieldId: PropTypes.bool,
  parentFieldId: PropTypes.string,
  loading: PropTypes.bool,
  options: PropTypes.array.isRequired,
  valueFieldId: PropTypes.string,
  labelFieldId: PropTypes.string,
  iconFieldId: PropTypes.string,
  imageFieldId: PropTypes.string,
  badgeFieldId: PropTypes.string,
  badgeColorFieldId: PropTypes.string,
  groupFieldId: 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,
  onSelect: PropTypes.func,
  placeholder: PropTypes.string,
  onOpen: PropTypes.func,
  onClose: PropTypes.func,
  multiSelect: PropTypes.bool,
  closePopupOnSelect: PropTypes.bool,
  hasCheckboxes: PropTypes.bool,
  format: PropTypes.string,
  collapseSelected: PropTypes.bool,
  lengthToGroup: PropTypes.number,
  onSearch: PropTypes.func,
  expandPopUp: PropTypes.bool,
  ajax(props, propName, componentName) {
    if (props[propName] && props.multiSelect) {
      return new Error(
        `Invalid prop \`${propName}\` supplied to` +
          ` \`${componentName}\`. Ajax and multiSelect together doesn't allowed.`
      );
    }
  },
  handleItemOpen: PropTypes.func,
};

InputSelectTree.defaultProps = {
  hasChildrenFieldId: 'hasChildren',
  parentFieldId: 'parentId',
  valueFieldId: 'id',
  labelFieldId: 'name',
  iconFieldId: 'icon',
  imageFieldId: 'image',
  badgeFieldId: 'badge',
  loading: false,
  disabled: false,
  disabledValues: [],
  resetOnBlur: false,
  filter: false,
  multiSelect: false,
  closePopupOnSelect: true,
  hasCheckboxes: false,
  collapseSelected: true,
  lengthToGroup: 3,
  ajax: false,
  expandPopUp: true,
  onSearch() {},
  onSelect() {},
  onToggle() {},
  onInput() {},
  onOpen() {},
  onClose() {},
  onChange() {},
  onScrollEnd() {},
  onElementCreate() {},
};

const fff = {
  className: 'n2o',
  prefixCls: '',
  animation: '',
  transitionName: '',
  choiceTransitionName: '',
  dropdownMatchSelectWidth: false,
  dropdownClassName: '',
  dropdownStyle: {},
  dropdownPopupAlign: null,
  onDropdownVisibleChange: () => true,
  notFoundContent: 'Ничего не найдено',
  showSearch: true,
  allowClear: false,
  maxTagTextLength: null,
  maxTagCount: null,
  maxTagPlaceholder: null,
  multiple: false,
  disabled: false,
  searchValue: '',
  defaultValue: null,
  value: null,
  labelInValue: false,
  onChange: null,
  onSelect: null,
  onSearch: null,
  onTreeExpand: null,
  showCheckedStrategy: SHOW_CHILD,
  treeIcon: false,
  treeLine: false,
  treeDefaultExpandAll: false,
  treeDefaultExpandedKeys: null,
  treeExpandedKeys: null,
  treeCheckable: false,
  treeCheckStrictly: false,
  filterTreeNode: Function,
  treeNodeFilterProp: 'value',
  treeNodeLabelProp: 'title',
  treeData: [
    { key: 1, pId: 0, label: 'test1', value: 'test1' },
    { key: 121, pId: 0, label: 'test2', value: 'test2' },
    { key: 11, pId: 1, label: 'test11', value: 'test11' },
    { key: 12, pId: 1, label: 'test12', value: 'test12' },
    { key: 111, pId: 11, label: 'test111', value: 'test111' },
  ],
  treeDataSimpleMode: false,
  loadData: null,
  getPopupContainer: () => document.body,
  autoClearSearchValue: true,
  inputIcon: null,
  clearIcon: null,
  removeIcon: null,
  switcherIcon: null,
};

export default onClickOutside(InputSelectTree);