UNPKG

react-bootstrap-typeahead

Version:
276 lines (275 loc) 10.7 kB
import isEqual from 'fast-deep-equal'; import PropTypes from 'prop-types'; import React from 'react'; import TypeaheadManager from './TypeaheadManager'; import { clearTypeahead, clickOrFocusInput, getInitialState, hideMenu, toggleMenu, } from './TypeaheadState'; import { caseSensitiveType, checkPropType, defaultInputValueType, defaultSelectedType, highlightOnlyResultType, ignoreDiacriticsType, isRequiredForA11y, labelKeyType, optionType, selectedType, } from '../propTypes'; import { addCustomOption, defaultFilterBy, getOptionLabel, getOptionProperty, getStringLabelKey, getUpdatedActiveIndex, getTruncatedOptions, isFunction, isShown, isString, noop, uniqueId, validateSelectedPropChange, } from '../utils'; import { DEFAULT_LABELKEY } from '../constants'; const propTypes = { allowNew: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), autoFocus: PropTypes.bool, caseSensitive: checkPropType(PropTypes.bool, caseSensitiveType), defaultInputValue: checkPropType(PropTypes.string, defaultInputValueType), defaultOpen: PropTypes.bool, defaultSelected: checkPropType(PropTypes.arrayOf(optionType), defaultSelectedType), filterBy: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.string.isRequired), PropTypes.func, ]), highlightOnlyResult: checkPropType(PropTypes.bool, highlightOnlyResultType), id: checkPropType(PropTypes.oneOfType([PropTypes.number, PropTypes.string]), isRequiredForA11y), ignoreDiacritics: checkPropType(PropTypes.bool, ignoreDiacriticsType), labelKey: checkPropType(PropTypes.oneOfType([PropTypes.string, PropTypes.func]), labelKeyType), maxResults: PropTypes.number, minLength: PropTypes.number, multiple: PropTypes.bool, onBlur: PropTypes.func, onChange: PropTypes.func, onFocus: PropTypes.func, onInputChange: PropTypes.func, onKeyDown: PropTypes.func, onMenuToggle: PropTypes.func, onPaginate: PropTypes.func, open: PropTypes.bool, options: PropTypes.arrayOf(optionType).isRequired, paginate: PropTypes.bool, selected: checkPropType(PropTypes.arrayOf(optionType), selectedType), }; const defaultProps = { allowNew: false, autoFocus: false, caseSensitive: false, defaultInputValue: '', defaultOpen: false, defaultSelected: [], filterBy: [], highlightOnlyResult: false, ignoreDiacritics: true, labelKey: DEFAULT_LABELKEY, maxResults: 100, minLength: 0, multiple: false, onBlur: noop, onFocus: noop, onInputChange: noop, onKeyDown: noop, onMenuToggle: noop, onPaginate: noop, paginate: true, }; function triggerInputChange(input, value) { const inputValue = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value'); inputValue && inputValue.set && inputValue.set.call(input, value); const e = new Event('input', { bubbles: true }); input.dispatchEvent(e); } class Typeahead extends React.Component { static propTypes = propTypes; static defaultProps = defaultProps; state = getInitialState(this.props); inputNode = null; isMenuShown = false; items = []; componentDidMount() { this.props.autoFocus && this.focus(); } componentDidUpdate(prevProps, prevState) { const { labelKey, multiple, selected } = this.props; validateSelectedPropChange(selected, prevProps.selected); if (selected && !isEqual(selected, prevState.selected)) { this.setState({ selected }); if (!multiple) { this.setState({ text: selected.length ? getOptionLabel(selected[0], labelKey) : '', }); } } } render() { const { onChange, ...props } = this.props; const mergedPropsAndState = { ...props, ...this.state }; const { filterBy, labelKey, options, paginate, shownResults, text } = mergedPropsAndState; this.isMenuShown = isShown(mergedPropsAndState); this.items = []; let results = []; if (this.isMenuShown) { const cb = (isFunction(filterBy) ? filterBy : defaultFilterBy); results = options.filter((option) => cb(option, mergedPropsAndState)); const shouldPaginate = paginate && results.length > shownResults; results = getTruncatedOptions(results, shownResults); if (addCustomOption(results, mergedPropsAndState)) { results.push({ customOption: true, [getStringLabelKey(labelKey)]: text, }); } if (shouldPaginate) { results.push({ [getStringLabelKey(labelKey)]: '', paginationOption: true, }); } } return (React.createElement(TypeaheadManager, { ...mergedPropsAndState, hideMenu: this.hideMenu, inputNode: this.inputNode, inputRef: this.inputRef, isMenuShown: this.isMenuShown, onActiveItemChange: this._handleActiveItemChange, onAdd: this._handleSelectionAdd, onBlur: this._handleBlur, onChange: this._handleInputChange, onClear: this._handleClear, onClick: this._handleClick, onFocus: this._handleFocus, onHide: this.hideMenu, onInitialItemChange: this._handleInitialItemChange, onKeyDown: this._handleKeyDown, onMenuItemClick: this._handleMenuItemSelect, onRemove: this._handleSelectionRemove, results: results, setItem: this.setItem, toggleMenu: this.toggleMenu })); } blur = () => { this.inputNode && this.inputNode.blur(); this.hideMenu(); }; clear = () => { this.setState(clearTypeahead); }; focus = () => { this.inputNode && this.inputNode.focus(); }; getInput = () => { return this.inputNode; }; inputRef = (inputNode) => { this.inputNode = inputNode; }; setItem = (item, position) => { this.items[position] = item; }; hideMenu = () => { this.setState(hideMenu); }; toggleMenu = () => { this.setState(toggleMenu); }; _handleActiveIndexChange = (activeIndex) => { this.setState((state) => ({ activeIndex, activeItem: activeIndex >= 0 ? state.activeItem : undefined, })); }; _handleActiveItemChange = (activeItem) => { if (!isEqual(activeItem, this.state.activeItem)) { this.setState({ activeItem }); } }; _handleBlur = (e) => { e.persist(); this.setState({ isFocused: false }, () => this.props.onBlur(e)); }; _handleChange = (selected) => { this.props.onChange && this.props.onChange(selected); }; _handleClear = () => { this.inputNode && triggerInputChange(this.inputNode, ''); this.setState(clearTypeahead, () => { if (this.props.multiple) { this._handleChange([]); } }); }; _handleClick = (e) => { e.persist(); const onClick = this.props.inputProps?.onClick; this.setState(clickOrFocusInput, () => isFunction(onClick) && onClick(e)); }; _handleFocus = (e) => { e.persist(); this.setState(clickOrFocusInput, () => this.props.onFocus(e)); }; _handleInitialItemChange = (initialItem) => { if (!isEqual(initialItem, this.state.initialItem)) { this.setState({ initialItem }); } }; _handleInputChange = (e) => { e.persist(); const text = e.currentTarget.value; const { multiple, onInputChange } = this.props; const shouldClearSelections = this.state.selected.length && !multiple; this.setState((state, props) => { const { activeIndex, activeItem, shownResults } = getInitialState(props); return { activeIndex, activeItem, selected: shouldClearSelections ? [] : state.selected, showMenu: true, shownResults, text, }; }, () => { onInputChange(text, e); shouldClearSelections && this._handleChange([]); }); }; _handleKeyDown = (e) => { const { activeItem } = this.state; if (!this.isMenuShown) { if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { this.setState({ showMenu: true }); } this.props.onKeyDown(e); return; } switch (e.key) { case 'ArrowUp': case 'ArrowDown': e.preventDefault(); this._handleActiveIndexChange(getUpdatedActiveIndex(this.state.activeIndex, e.key, this.items)); break; case 'Enter': e.preventDefault(); activeItem && this._handleMenuItemSelect(activeItem, e); break; case 'Escape': case 'Tab': this.hideMenu(); break; default: break; } this.props.onKeyDown(e); }; _handleMenuItemSelect = (option, e) => { if (getOptionProperty(option, 'paginationOption')) { this._handlePaginate(e); } else { this._handleSelectionAdd(option); } }; _handlePaginate = (e) => { e.persist(); this.setState((state, props) => ({ shownResults: state.shownResults + props.maxResults, }), () => this.props.onPaginate(e, this.state.shownResults)); }; _handleSelectionAdd = (option) => { const { multiple, labelKey } = this.props; let selected; let selection = option; let text; if (!isString(selection) && selection.customOption) { selection = { ...selection, id: uniqueId('new-id-') }; } if (multiple) { selected = this.state.selected.concat(selection); text = ''; } else { selected = [selection]; text = getOptionLabel(selection, labelKey); } this.setState((state, props) => ({ ...hideMenu(state, props), initialItem: selection, selected, text, }), () => this._handleChange(selected)); }; _handleSelectionRemove = (selection) => { const selected = this.state.selected.filter((option) => !isEqual(option, selection)); this.focus(); this.setState((state, props) => ({ ...hideMenu(state, props), selected, }), () => this._handleChange(selected)); }; } export default Typeahead;