UNPKG

extpoint-yii2

Version:

JavaScript part for projects on ExtPoint Yii2 Boilerplate and yii2-core

454 lines (396 loc) 15.9 kB
import React from 'react'; import {findDOMNode} from 'react-dom'; import PropTypes from 'prop-types'; import enhanceWithClickOutside from 'react-click-outside'; import {connect} from 'react-redux'; import {change} from 'redux-form'; import _uniq from 'lodash-es/uniq'; import _remove from 'lodash-es/remove'; import _filter from 'lodash-es/filter'; import _isArray from 'lodash-es/isArray'; import _isString from 'lodash-es/isString'; import _isEqual from 'lodash-es/isEqual'; import _get from 'lodash-es/get'; import _isObject from 'lodash-es/isObject'; import _isUndefined from 'lodash-es/isUndefined'; import _find from 'lodash-es/find'; import {types, view, locale} from 'components'; import {fetchByIds, fetchAutoComplete, cacheEntries} from 'actions/formList'; import {getLabels, getAutoComplete} from '../reducers/formList'; class DropDownField extends React.Component { static propTypes = { fieldId: PropTypes.string.isRequired, placeholder: PropTypes.string, searchPlaceholder: PropTypes.string, input: PropTypes.shape({ name: PropTypes.string, value: PropTypes.any, onChange: PropTypes.func, }), multiple: PropTypes.bool, disabled: PropTypes.bool, enumClassName: PropTypes.oneOfType([ PropTypes.string, PropTypes.func, ]), autoComplete: PropTypes.oneOfType([ PropTypes.bool, PropTypes.string, PropTypes.shape({ method: PropTypes.string, }), ]), autoCompleteFetch: PropTypes.bool, autoSelectFirst: PropTypes.bool, onChange: PropTypes.func, items: PropTypes.oneOfType([ PropTypes.array, PropTypes.object, PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.number, label: PropTypes.oneOfType([ PropTypes.number, PropTypes.string, ]), })), ]), attribute: PropTypes.string, modelClass: PropTypes.string, autoCompleteItems: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.number, label: PropTypes.string, })), valueLabels: PropTypes.object, }; static defaultProps = { placeholder: locale.t('Выбрать'), searchPlaceholder: locale.t('Начните вводить символы для поиска...'), autoSelectFirst: false, }; constructor() { super(...arguments); this._onKeyDown = this._onKeyDown.bind(this); this._searchRequestTimeout = null; this.state = { query: '', isOpened: false, isFocused: false, hoveredValue: null, }; this.initItems(this.props); } initItems(props) { let allItems = {}; if (props.items) { allItems = props.items; } else if (props.enumClassName) { allItems = types.getEnumLabels(props.enumClassName); } // Convert to array if (!_isArray(allItems) && _isObject(allItems)) { allItems = Object.keys(allItems).map(key => ({ id: key, label: allItems[key], })); } allItems = allItems.map(item => { return _isObject(item) ? item : { id: item, label: item, }; }); this.state.allItems = allItems; this.state.filteredItems = allItems; } componentWillMount() { const values = this.getValues(); // Select first value on mount if (this.props.autoSelectFirst && this.props.input) { if (values.length === 0) { if (this.state.filteredItems.length > 0) { const id = this.state.filteredItems[0].id; const value = this.props.multiple ? [id] : id; this.props.dispatch(change(this.props.formId, this.props.input.name, value)); } } } // Async load selected labels from backend if (values.length > 0 && !this.getLabel()) { this.props.dispatch(fetchByIds(this.props.fieldId, values, { model: this.props.modelClass, attribute: this.props.attribute, })); } if (this.props.autoCompleteFetch) { this._checkToAutoFetch(); } } componentDidMount() { window.addEventListener('keydown', this._onKeyDown); } componentWillReceiveProps(nextProps) { if (this.props.items !== nextProps.items) { this.initItems(nextProps); } // If autoComplete, autoCompleteFetch and autoSelectFirst is set, then set input to first item of // the autocomplete results if (_isString(this.props.autoComplete) || _isObject(this.props.autoComplete) && this.props.autoCompleteFetch && !this.state.isOpened && this.props.autoSelectFirst && this.props.autoCompleteItems !== nextProps.autoCompleteItems && nextProps.autoCompleteItems.length > 0 ) { const id = nextProps.autoCompleteItems[0].id; const value = this.props.multiple ? [id] : id; this.props.dispatch(change(this.props.formId, this.props.input.name, value)); } } componentDidUpdate(prevProps, prevState) { if (this.state.isOpened && !prevState.isOpened) { // Reset items on open this.setState({filteredItems: this.state.allItems}); // Set focus on search input on open drop down if (this.props.autoComplete) { const searchInput = findDOMNode(this.refs.dropDown).querySelector('[type=search]'); if (searchInput) { searchInput.focus(); } } } // Store entries in cache for render labels if (this.props.input && prevProps.input.value !== this.props.input.value) { const values = this.getValues(); if (values.length > 0) { this.props.dispatch(cacheEntries(this.props.fieldId, values)); } } // Check auto fetch on change config if (!prevProps.autoCompleteFetch && this.props.autoCompleteFetch) { this._checkToAutoFetch(); } else { // Check auto fetch on change condition if (this.props.autoCompleteFetch && _isObject(this.props.autoComplete) && !_isEqual(_get(prevProps, 'autoComplete.condition'), this.props.autoComplete.condition)) { this._checkToAutoFetch(); } } } componentWillUnmount() { window.removeEventListener('keydown', this._onKeyDown); } handleClickOutside() { this.setState({ isOpened: false }); } getFilteredItems() { if (_isString(this.props.autoComplete) || _isObject(this.props.autoComplete)) { // Show selected values on render field if (this.state.query === '' && this.props.valueLabels) { return Object.keys(this.props.valueLabels).map(id => ({ id: /^[0-9]+$/.test(id) ? parseInt(id) : id, label: this.props.valueLabels[id], })); } return this.props.autoCompleteItems || []; } return this.state.filteredItems; } /** * @return {[]} */ getValues() { const value = this.props.input && this.props.input.value; if (!value && value !== 0) { return []; } return !_isArray(value) ? [value] : value; } /** * @return {string} */ getLabel() { const values = this.getValues(); if (values.length === 0) { return ''; } const labels = []; const filteredItems = this.getFilteredItems(); values.map(value => { if (this.props.valueLabels && [value]) { labels.push(this.props.valueLabels[value]); } else { const item = _find(filteredItems, item => item.id === value) || _find(this.state.allItems, item => item.id === value); if (item) { labels.push(item.label || item.id); } } }); return labels.join(', '); } render() { const values = this.getValues(); const {input, disabled, onChange, ...props} = this.props; // eslint-disable-line no-unused-vars const DropDownFieldView = this.props.view || view.getFormView('DropDownFieldView'); return ( <span> <DropDownFieldView {...props} ref='dropDown' inputProps={{ type: 'text', value: this.getLabel(), placeholder: this.props.placeholder, readOnly: true, disabled, onClick: () => !disabled && this.setState({isOpened: !this.state.isOpened}) }} searchInputProps={{ type: 'search', placeholder: locale.t('Поиск'), onChange: e => this.search(e.target.value), tabIndex: -1 }} searchHint={( (_isString(this.props.autoComplete) || _isObject(this.props.autoComplete)) && !this.state.query && this.props.autoCompleteItems && this.props.autoCompleteItems.length === 0 ? this.props.searchPlaceholder : null )} onReset={!this.props.autoSelectFirst && values.length > 0 ? () => this._onChange(null) : null} isOpened={this.state.isOpened} isShowSearch={!!this.props.autoComplete} items={this.getFilteredItems().map(item => ({ id: item.id, label: item.label, item, isChecked: values.indexOf(item.id) !== -1, isHovered: this.state.hoveredValue === item.id, isShowCheckbox: this.props.multiple, inputProps: { type: 'checkbox', checked: values.indexOf(item.id) !== -1, }, onClick: () => this.toggleItem(item.id), onMouseOver: () => this.setState({hoveredValue: item.id}), }))} /> </span> ); } toggleItem(key) { const values = this.props.multiple ? _uniq(this.getValues().concat([key])) : [key]; if (this.props.multiple) { const prevValues = this.getValues(); const isSelected = prevValues.indexOf(key) !== -1; if (isSelected) { _remove(values, item => item === key); } } const value = this.props.multiple ? values : values[0]; this._onChange(value); this.setState({ isOpened: this.props.multiple ? this.state.isOpened : false, }); } _checkToAutoFetch() { const values = this.getValues(); if (values.length === 0 && (_isString(this.props.autoComplete) || _isObject(this.props.autoComplete))) { this.search('', true); } } _onChange(value) { if (this.props.input) { this.props.input.onChange(value); } this.props.onChange && this.props.onChange(value); } /** * @param {string} query * @param {boolean} force */ search(query, force) { this.setState({query}); query = query.toLowerCase(); if (_isString(this.props.autoComplete) || _isObject(this.props.autoComplete)) { if (this._searchRequestTimeout) { clearTimeout(this._searchRequestTimeout); } this._searchRequestTimeout = setTimeout(() => { this.props.dispatch(fetchAutoComplete(this.props.fieldId, query, force, { ...this.props.autoComplete, model: this.props.modelClass, attribute: this.props.attribute, })); }, 250); } else { this.setState({ filteredItems: query ? _filter(this.getFilteredItems(), item => item.label.toLowerCase().indexOf(query) === 0) : this.state.allItems, }); } } /** * @param {number} direction */ moveHover(direction) { direction = direction > 0 ? 1 : -1; const keys = this.getFilteredItems().map(item => item.id); const index = this.state.hoveredValue ? keys.indexOf(this.state.hoveredValue) : -1; const newIndex = index !== -1 ? Math.min(keys.length - 1, Math.max(0, index + direction)) : 0; this.setState({ hoveredValue: keys[newIndex], }); } _onKeyDown(e) { if (!this.state.isFocused && !this.state.isOpened) { return; } switch (e.which) { case 9: // tab case 27: // esc e.preventDefault(); this.setState({ isOpened: false, }); break; case 13: // enter if (this.state.isOpened) { e.preventDefault(); if (this.state.hoveredValue) { this.toggleItem(this.state.hoveredValue); } else { // Select first result const items = this.getFilteredItems(); if (items.length > 0) { this.toggleItem(items[0].id); } } } break; case 38: // arrow up e.preventDefault(); this.moveHover(-1); break; case 40: // arrow down e.preventDefault(); if (!this.state.isOpened) { this.setState({ isOpened: true, }); } else { this.moveHover(1); } break; } } } export default connect( (state, props) => ({ multiple: props.multiple || (props.metaItem ? props.metaItem.multiple : null), enumClassName: !_isUndefined(props.enumClassName) ? props.enumClassName : (props.metaItem ? props.metaItem.enumClassName : null), autoComplete: !_isUndefined(props.autoComplete) ? props.autoComplete : (props.metaItem ? props.metaItem.autoComplete : null), autoCompleteItems: getAutoComplete(state, props.fieldId), valueLabels: getLabels(state, props.fieldId, [].concat(props.input && props.input.value || [])), }) )( enhanceWithClickOutside(DropDownField) );