components.controls.AutoComplete.AutoComplete.tsx Maven / Gradle / Ivy
The newest version!
import React from 'react'
import find from 'lodash/find'
import isEmpty from 'lodash/isEmpty'
import isFunction from 'lodash/isFunction'
import filter from 'lodash/filter'
import includes from 'lodash/includes'
import isEqual from 'lodash/isEqual'
import map from 'lodash/map'
import isArray from 'lodash/isArray'
import isNil from 'lodash/isNil'
import pick from 'lodash/pick'
import onClickOutside from 'react-onclickoutside'
import classNames from 'classnames'
import { Manager, Reference, Popper } from 'react-popper'
import { BadgeType, PopupList } from '@i-novus/n2o-components/lib/inputs/InputSelect/PopupList'
import { InputContent } from '@i-novus/n2o-components/lib/inputs/InputSelect/InputContent'
import { InputSelectGroup } from '@i-novus/n2o-components/lib/inputs/InputSelect/InputSelectGroup'
import { Alert } from '@i-novus/n2o-components/lib/display/Alerts/Alert'
import { Filter, TOption } from '@i-novus/n2o-components/lib/inputs/InputSelect/types'
import listContainer from '../listContainer'
type State = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any[]
input: string
options?: TOption[]
isExpanded: boolean
activeValueId: string
}
const DEFAULT_DATA_SEARCH_DELAY = 400
class AutoComplete extends React.Component {
input: Element | null
constructor(props: Props) {
super(props)
this.state = {
isExpanded: false,
value: [],
input: props.value && !props.tags ? props.value : '',
activeValueId: '',
}
this.input = null
}
componentDidMount() {
const { value, tags } = this.props
if (!isEmpty(value)) {
const currentValue = isArray(value) ? value : [value]
this.setState({
value: value ? currentValue : [],
input: value && !tags ? value : '',
})
}
}
// eslint-disable-next-line
componentDidUpdate = (prevProps: Props, prevState: State) => {
const { value, options, tags } = this.props
const compareListProps = ['options', 'value']
const compareListState = ['input']
if (
!isEqual(
pick(prevProps, compareListProps),
pick(this.props, compareListProps),
) || !isEqual(
pick(prevState, compareListState),
pick(this.state, compareListState),
)
) {
const state = {} as State
if (!isEqual(prevProps.options, options)) {
state.options = options
}
if (prevProps.value !== value) {
const currentValue = isArray(value) ? value : [value]
state.value = value ? currentValue : []
state.input = value && !tags ? value : ''
}
if (!isEmpty(state)) {
this.setState(state)
}
}
}
/**
* Обрабатывает клик за пределы компонента
* вызывается библиотекой react-onclickoutside
*/
handleClickOutside = () => {
const { isExpanded } = this.state
if (isExpanded) {
this.setIsExpanded(false)
this.onBlur()
}
}
// eslint-disable-next-line consistent-return
calcPopperWidth = () => {
const { input } = this
const { popupAutoSize } = this.props
if (input && !popupAutoSize) {
return input.getBoundingClientRect().width
}
}
setIsExpanded = (isExpanded: State['isExpanded']) => {
const { disabled, onToggle, onClose, fetchData } = this.props
const { isExpanded: previousIsExpanded } = this.state
if (!disabled && isExpanded !== previousIsExpanded) {
this.setState({ isExpanded })
onToggle(isExpanded)
if (isExpanded) {
fetchData({ page: 1 })
} else {
onClose()
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setInputRef = (popperRef: any) => (r: any) => {
this.input = r
popperRef(r)
}
onFocus = () => {
const { openOnFocus } = this.props
if (openOnFocus) {
this.setIsExpanded(true)
}
}
onClick = () => {
this.setIsExpanded(true)
}
handleDataSearch: Props['onSearch'] = (input, delay, callback) => {
const { onSearch, filter, labelFieldId, options } = this.props
if (filter && ['includes', 'startsWith', 'endsWith'].includes(filter as Filter)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const filterFunc = (item: TOption) => (String.prototype[filter as any] as any).call(item, input)
const filteredOptions = options.filter(item => filterFunc(item[labelFieldId as keyof TOption]))
this.setState({ options: filteredOptions })
} else {
// серверная фильтрация
const { value } = this.state
const labels = map(value, item => item[labelFieldId])
if (labels.some(label => label === input)) {
onSearch('', delay || DEFAULT_DATA_SEARCH_DELAY, callback)
} else {
onSearch(input, delay || DEFAULT_DATA_SEARCH_DELAY, callback)
}
}
}
onChange = (userInput: State['input']) => {
const { onInput, tags, onChange } = this.props
const { input } = this.state
const onSetNewInputValue = (input: State['input']) => {
onInput(input)
if (!tags && input === '') {
onChange([])
} else if (!tags) {
onChange([input])
}
this.handleDataSearch(input)
}
if (!isEqual(input, userInput)) {
const getSelected = (prevState: State) => {
if (tags) {
return prevState.value
}
// ToDo здесь вероятно баг, удалено лишнее
return [userInput]
}
this.setState(
prevState => ({
input: userInput,
value: getSelected(prevState),
isExpanded: true,
}),
() => onSetNewInputValue(userInput),
)
}
}
onBlur = () => {
const { onBlur } = this.props
const { value } = this.state
if (isFunction(onBlur)) {
onBlur(value)
}
}
onSelect = (item: TOption) => {
if (!item) {
return
}
const { onChange, closePopupOnSelect, tags, labelFieldId } = this.props
this.setState(
prevState => ({
value: tags ? [...prevState.value, item] : [item],
input: !tags ? item[labelFieldId as keyof TOption] : '',
}),
() => {
const { value, input } = this.state
if (closePopupOnSelect) {
this.setIsExpanded(false)
}
if (typeof item === 'string') {
this.forceUpdate()
}
if (tags) {
onChange(value)
return
}
onChange(input)
},
)
}
handleElementClear = () => {
const { onChange, onBlur } = this.props
const { input, value } = this.state
this.setState(
{
input: '',
value: [],
},
() => {
this.handleDataSearch(input)
onChange(value)
onBlur(null)
},
)
}
setActiveValueId = (activeValueId: State['activeValueId']) => {
this.setState({ activeValueId })
}
removeSelectedItem = (item: TOption, index: number | null = null) => {
const { onChange } = this.props
const { value } = this.state
let newValue = value.slice()
if (!isNil(index)) {
newValue.splice(index, 1)
} else {
newValue = value.slice(0, value.length - 1)
}
this.setState({ value: newValue }, () => {
onChange(newValue)
this.forceUpdate()
})
}
onButtonClick = () => {
if (this.input) {
(this.input as HTMLInputElement).focus()
}
}
render() {
const { isExpanded, value, activeValueId, input } = this.state
const {
loading,
className,
valueFieldId,
labelFieldId,
iconFieldId,
disabled,
placeholder,
disabledValues,
imageFieldId,
groupFieldId,
hasCheckboxes,
format,
badge,
fetchData,
page,
style,
alerts,
autoFocus,
options,
tags,
maxTagTextLength,
onDismiss,
size,
count,
quickSearchParam,
} = this.props
const needAddFilter = !find(value, item => item[labelFieldId] === input)
const filteredOptions = filter(
options,
item => includes(item[labelFieldId as keyof TOption], input) || isEmpty(input),
)
const filterValue = isEmpty(input) ? {} : { [quickSearchParam || labelFieldId]: input }
return (
{({ ref }) => (
this.setIsExpanded(false)}
openPopUp={() => this.setIsExpanded(true)}
selected={value}
value={input}
onFocus={this.onFocus}
onClick={this.onClick}
onRemoveItem={this.removeSelectedItem}
isExpanded={isExpanded}
valueFieldId={valueFieldId}
activeValueId={activeValueId}
onSelect={this.onSelect}
disabled={disabled}
disabledValues={disabledValues}
placeholder={placeholder}
labelFieldId={labelFieldId}
autoFocus={autoFocus}
/>
)}
{isExpanded && !isEmpty(filteredOptions) && (
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{({ ref, placement, scheduleUpdate }: any) => (
{alerts?.map(alert => (
onDismiss(alert.id)}
{...alert}
/>
))}
)}
)}
)
}
static defaultProps = {
valueFieldId: 'label',
labelFieldId: 'name',
iconFieldId: 'icon',
imageFieldId: 'image',
loading: false,
disabled: false,
disabledValues: [],
resetOnBlur: false,
filter: false,
multiSelect: false,
closePopupOnSelect: true,
hasCheckboxes: false,
expandPopUp: false,
flip: false,
autoFocus: false,
popupAutoSize: false,
tags: false,
options: [],
value: {},
descriptionFieldId: '',
enabledFieldId: '',
statusFieldId: '',
groupFieldId: '',
sortFieldId: '',
onSearch() {},
onSelect() {},
onToggle() {},
onInput() {},
fetchData() {},
onClose() {},
onChange() {},
onBlur() {},
onDismiss() {},
} as Props
}
type Props = {
/**
* Стили
*/
style?: object,
/**
* Флаг загрузки
*/
loading: boolean,
/**
* Массив данных
*/
options: TOption[],
/**
* Ключ значения
*/
valueFieldId: string,
/**
* Ключ отображаемого значения
*/
labelFieldId: string,
/**
* Ключ icon в данных
*/
iconFieldId: string,
/**
* Ключ image в данных
*/
imageFieldId: string,
/**
* Данные для badge
*/
badge?: BadgeType,
/**
* Флаг активности
*/
disabled: boolean,
/**
* Неактивные данные
*/
disabledValues?: Array,
/**
* Значение
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
/**
* Callback на переключение
*/
onToggle(arg: boolean): void,
onInput(input: string): void,
/**
* Callback на изменение
*/
onChange(value: Props['value']): void,
/**
* Callback на выбор
*/
onSelect(): void,
/**
* Placeholder контрола
*/
placeholder?: string,
/**
* Фича, при которой сбрасывается значение контрола, если оно не выбрано из popup
*/
resetOnBlur: boolean,
/**
* Callback на открытие
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetchData(arg: any): void,
/**
* Callback на закрытие
*/
onClose(): void,
/**
* Мульти выбор значений
*/
multiSelect: boolean,
/**
* Поле для группировки
*/
groupFieldId: string,
/**
* Флаг закрытия попапа при выборе
*/
closePopupOnSelect: boolean,
/**
* Флаг наличия чекбоксов в селекте
*/
hasCheckboxes: boolean,
/**
* Формат
*/
format?: string,
/**
* Callback на поиск
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSearch(input: State['input'], delay?: number, callback?: any): void,
expandPopUp: boolean,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
alerts?: any[],
/**
* Авто фокусировка на селекте
*/
autoFocus: boolean,
/**
* Флаг авто размера попапа
*/
popupAutoSize: boolean,
/**
* Мод работы Autocomplete
*/
tags: boolean,
/**
* Максимальная длина текста в тэге, до усечения
*/
maxTagTextLength?: number,
openOnFocus?: boolean,
filter: Filter | boolean,
className?: string,
onBlur(value: Props['value']): void,
onDismiss(id: string | number): void,
flip: boolean,
page?: number,
size?: number,
count?: number,
quickSearchParam?: string,
}
export { AutoComplete }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default listContainer(onClickOutside(AutoComplete as any) as any)