Home Reference Source

src/components/widgets/List/List.jsx

import React, { Component } from 'react';
import cn from 'classnames';
import ReactDom from 'react-dom';
import { map, isEqual, isEmpty } from 'lodash';
import PropTypes from 'prop-types';
import ListItem from './ListItem';
import ListMoreButton from './ListMoreButton';
import {
  WindowScroller,
  AutoSizer,
  CellMeasurer,
  CellMeasurerCache,
  List as Virtualizer,
} from 'react-virtualized';
import { getIndex } from '../Table/Table';

const SCROLL_OFFSET = 100;

/**
 * Компонент List
 * @reactProps {number|string} selectedId - id выбранной записи
 * @reactProps {boolean} autoFocus - фокус при инициализации на перой или на selectedId строке
 * @reactProps {function} onItemClick - callback при клике на строку
 * @reactProps {object} rowClick - кастомное действие клика
 * @reactProps {function} onFetchMore - callback при клика на "Загрузить еще" или скролле
 * @reactProps {string|number} selectedId - id выбранной записи
 */
class List extends Component {
  constructor(props) {
    super(props);
    this.state = {
      selectedIndex: props.hasSelect
        ? getIndex(props.data, props.selectedId)
        : null,
      data: props.data,
    };
    this.cache = new CellMeasurerCache({
      fixedWidth: true,
      defaultHeight: 90,
    });

    this._scrollTimeoutId = null;

    this.renderRow = this.renderRow.bind(this);
    this.onItemClick = this.onItemClick.bind(this);
    this.fetchMore = this.fetchMore.bind(this);
    this.onScroll = this.onScroll.bind(this);
    this.setListContainerRef = this.setListContainerRef.bind(this);
    this._setVirtualizerRef = this._setVirtualizerRef.bind(this);
    this._setWindowScrollerRef = this._setWindowScrollerRef.bind(this);
  }

  componentDidMount() {
    const { fetchOnScroll } = this.props;
    if (fetchOnScroll) {
      this._listContainer.addEventListener('scroll', this.onScroll, true);
    }
  }

  componentDidUpdate(prevProps) {
    const {
      data,
      hasMoreButton,
      fetchOnScroll,
      maxHeight,
      selectedId,
    } = this.props;
    if (!isEqual(prevProps, this.props)) {
      let state = {};
      if (hasMoreButton && !fetchOnScroll && !isEqual(prevProps.data, data)) {
        if (maxHeight) {
          this._virtualizer.scrollToRow(data.length);
        } else {
          const virtualizer = ReactDom.findDOMNode(this._virtualizer);
          if (virtualizer) {
            window.scrollTo(0, virtualizer.scrollHeight);
          }
        }
      }

      if (!isEqual(prevProps.data, data)) {
        state = {
          ...state,
          data: hasMoreButton ? [...data, {}] : data,
        };
      }

      if (selectedId && !isEqual(prevProps.selectedId, selectedId)) {
        state = {
          ...state,
          selectedIndex: getIndex(data, selectedId),
        };
      }

      this.setState(
        state,
        () => this._virtualizer && this._virtualizer.forceUpdateGrid()
      );
    }
  }

  componentWillUnmount() {
    const { fetchOnScroll } = this.props;
    if (fetchOnScroll) {
      this._listContainer.removeEventListener('scroll', this.onScroll);
    }
  }

  setListContainerRef(el) {
    this._listContainer = el;
  }

  _setWindowScrollerRef(el) {
    this._windowScroller = el;
  }

  _setVirtualizerRef(el) {
    this._virtualizer = el;
  }

  onItemClick(index) {
    const { onItemClick, rowClick, hasSelect } = this.props;
    if (!rowClick && hasSelect) {
      this.setState({ selectedIndex: index }, () => {
        if (this._virtualizer) {
          this._virtualizer.forceUpdateGrid();
        }
      });
    }
    onItemClick(index);
  }

  fetchMore() {
    const { onFetchMore } = this.props;
    onFetchMore();
  }

  onScroll(event) {
    clearTimeout(this._scrollTimeoutId);

    this._scrollTimeoutId = setTimeout(() => {
      const scrollPosition = event.target.scrollTop + event.target.clientHeight;
      const minScrollToLoad = event.target.scrollHeight - SCROLL_OFFSET;
      if (
        scrollPosition >= minScrollToLoad ||
        scrollPosition === event.target.scrollHeight
      ) {
        this.fetchMore();
      }
    }, 300);
  }

  renderRow({ index, key, style, parent }) {
    const { divider, hasMoreButton, fetchOnScroll, hasSelect } = this.props;
    const { data } = this.state;
    let moreBtn = null;
    if (index === data.length - 1 && hasMoreButton && !fetchOnScroll) {
      return (
        <CellMeasurer
          key={key}
          cache={this.cache}
          parent={parent}
          columnIndex={0}
          rowIndex={index}
        >
          <ListMoreButton style={style} onClick={this.fetchMore} />
        </CellMeasurer>
      );
    }

    return (
      <React.Fragment>
        <CellMeasurer
          key={key}
          cache={this.cache}
          parent={parent}
          columnIndex={0}
          rowIndex={index}
        >
          <ListItem
            {...data[index]}
            hasSelect={hasSelect}
            key={key}
            style={style}
            divider={divider}
            selected={this.state.selectedIndex === index}
            onClick={() => this.onItemClick(index)}
          />
        </CellMeasurer>
        {moreBtn}
      </React.Fragment>
    );
  }

  render() {
    const { className, maxHeight } = this.props;
    const { data } = this.state;
    return (
      <div
        ref={this.setListContainerRef}
        className={cn('n2o-widget-list', className)}
      >
        {(!data || isEmpty(data)) && (
          <div className="n2o-widget-list--empty-view text-muted">
            Нет данных для отображения
          </div>
        )}
        {data && !isEmpty(data) && (
          <div className="n2o-widget-list-container">
            {maxHeight ? (
              <AutoSizer style={{ height: '100%' }}>
                {({ width }) => (
                  <Virtualizer
                    ref={this._setVirtualizerRef}
                    width={width}
                    height={maxHeight}
                    deferredMeasurementCache={this.cache}
                    rowHeight={this.cache.rowHeight}
                    rowRenderer={this.renderRow}
                    rowCount={data.length}
                    overscanRowCount={5}
                  />
                )}
              </AutoSizer>
            ) : (
              <WindowScroller
                ref={this._setWindowScrollerRef}
                scrollElement={window}
              >
                {({
                  height,
                  isScrolling,
                  registerChild,
                  onChildScroll,
                  scrollTop,
                }) => (
                  <AutoSizer style={{ height: '100%' }}>
                    {({ width }) => (
                      <Virtualizer
                        ref={this._setVirtualizerRef}
                        autoHeight
                        height={height}
                        isScrolling={isScrolling}
                        onScroll={onChildScroll}
                        overscanRowCount={5}
                        rowCount={data.length}
                        rowHeight={this.cache.rowHeight}
                        rowRenderer={this.renderRow}
                        scrollTop={scrollTop}
                        width={width}
                      />
                    )}
                  </AutoSizer>
                )}
              </WindowScroller>
            )}
          </div>
        )}
      </div>
    );
  }
}

List.propTypes = {
  onItemClick: PropTypes.func,
  hasSelect: PropTypes.bool,
  className: PropTypes.string,
  data: PropTypes.arrayOf(PropTypes.object),
  rowClick: PropTypes.object,
  hasMoreButton: PropTypes.bool,
  onFetchMore: PropTypes.func,
  maxHeight: PropTypes.number,
  fetchOnScroll: PropTypes.bool,
  divider: PropTypes.bool,
  selectedId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
List.defaultProps = {
  onItemClick: () => {},
  onFetchMore: () => {},
  hasSelect: false,
  data: [],
  rowClick: false,
  hasMoreButton: false,
  fetchOnScroll: false,
  divider: true,
};

export default List;