UNPKG

wix-style-react

Version:
278 lines • 11.5 kB
import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import InputWithOptions from '../InputWithOptions'; import InputWithTags from './InputWithTags'; import last from 'lodash/last'; import difference from 'difference'; import { st, classes } from './MultiSelect.st.css'; class MultiSelect extends InputWithOptions { constructor(props) { super(props); this.onKeyDown = this.onKeyDown.bind(this); this.onPaste = this.onPaste.bind(this); this._onBlur = this._onBlur.bind(this); this.state = { ...this.state, pasteDetected: false }; } hideOptions() { super.hideOptions(); if (this.props.clearOnBlur) { this.clearInput(); } } rootAdditionalProps() { const { className } = this.props; return { className: st(classes.root, className), }; } onClickOutside() { if (this.state.showOptions) { this.hideOptions(); } } _onBlur(event) { super._onBlur(event); this.props.acceptOnBlur && this.submitValue(this.state.inputValue); } getUnselectedOptions() { const optionIds = this.props.options.map(option => option.id); const tagIds = this.props.tags.map(tag => tag.id); const unselectedOptionsIds = difference(optionIds, tagIds); return this.props.options.filter(option => unselectedOptionsIds.includes(option.id)); } dropdownAdditionalProps() { const { predicate, emptyStateMessage, fixedFooter } = this.props; const filterFunc = this.state.isEditing ? predicate : () => true; const filtered = this.getUnselectedOptions().filter(filterFunc); let options = filtered; if (emptyStateMessage && filtered.length === 0) { options = [ { id: 'empty-state-message', value: emptyStateMessage, disabled: true, }, ]; } return { options, closeOnSelect: false, selectedHighlight: false, selectedId: -1, fixedFooter, }; } closeOnSelect() { return false; } inputAdditionalProps() { return { readOnly: this.props.readOnly, disableEditing: true, inputElement: (React.createElement(InputWithTags, { className: classes.inputWithTags, onReorder: this.props.onReorder, maxNumRows: this.props.maxNumRows, mode: this.props.mode, hideCustomSuffix: this.isDropdownLayoutVisible(), customSuffix: this.props.customSuffix, border: this.props.border })), onKeyDown: this.onKeyDown, delimiters: this.props.delimiters, onPaste: this.onPaste, }; } onPaste() { this.setState({ pasteDetected: true }); } _splitByDelimitersAndTrim(value) { const delimitersRegexp = new RegExp(this.props.delimiters.join('|'), 'g'); return value .split(delimitersRegexp) .map(str => str.trim()) .filter(str => str); } _onChange(event) { if (this.state.pasteDetected) { const value = event.target.value; this.setState({ pasteDetected: false }, () => { this.submitValue(value); }); } else { this.setState({ inputValue: event.target.value }); this.props.onChange && this.props.onChange(event); } // If the input value is not empty, should show the options if (event.target.value.trim()) { this.showOptions(); } } _onSelect(option) { this.onSelect(option); } _onManuallyInput(inputValue, event) { const { value } = this.props; // FIXME: InputWithOptions is not updating it's inputValue state when the `value` prop changes. // So using `value` here, covers for that bug. (This is tested) // BTW: Previously, `value` was used to trigger onSelect, and `inputValue` was used to trigger onManuallyInput. Which is crazy. // So now both of them trigger a submit (onManuallyInput). const _value = (value && value.trim()) || (inputValue && inputValue.trim()); this.submitValue(_value); _value && event.preventDefault(); if (this.closeOnSelect()) { this.hideOptions(); } } getManualSubmitKeys() { return ['Enter', 'Tab'].concat(this.props.delimiters); } onKeyDown(event) { const { tags, value, onRemoveTag } = this.props; if (tags.length > 0 && (event.key === 'Delete' || event.key === 'Backspace') && value && value.length === 0) { onRemoveTag(last(tags).id); } if (event.key === 'Escape') { this.clearInput(); super.hideOptions(); } if (this.props.onKeyDown) { this.props.onKeyDown(event); } } optionToTag({ id, value, tag, theme }) { return tag ? { id, ...tag } : { id, label: value, theme }; } onSelect(option) { this.clearInput(); const { onSelect } = this.props; if (onSelect) { onSelect(this.props.options.find(o => o.id === option.id)); } } submitValue(inputValue) { if (!inputValue) { return; } const { onManuallyInput } = this.props; const values = this._splitByDelimitersAndTrim(inputValue); onManuallyInput && values.length && onManuallyInput(values); this.clearInput(); } clearInput() { this.input.current && this.input.current.clear(); if (this.props.onChange) { this.props.onChange({ target: { value: '' } }); } } } MultiSelect.autoSizeInput = ({ className, 'data-ref': dataRef, ...rest }) => { const inputClassName = classNames(className, classes.autoSizeInput); return React.createElement("input", { ...rest, ref: dataRef, className: inputClassName }); }; MultiSelect.autoSizeInputWithRef = () => React.forwardRef((props, ref) => (({ className, ref, ...rest }) => { const inputClassName = classNames(className, classes.autoSizeInput); return React.createElement("input", { ...rest, ref: ref, className: inputClassName }); })({ ...props, ref })); MultiSelect.displayName = 'MultiSelect'; MultiSelect.propTypes = { /** Associate a control with the regions that it controls.*/ ariaControls: PropTypes.string, /** Associate a region with its descriptions. Similar to aria-controls but instead associating descriptions to the region and description identifiers are separated with a space.*/ ariaDescribedby: PropTypes.string, /** Define a string that labels the current element in case where a text label is not visible on the screen. */ ariaLabel: PropTypes.string, /** Control the border style of input */ border: PropTypes.oneOf(['standard', 'round', 'bottomLine', 'none']), /** Closes list once list item is selected */ closeOnSelect: PropTypes.bool, /** Allows to pass all common popover props. */ popoverProps: PropTypes.shape({ appendTo: PropTypes.oneOf(['window', 'scrollParent', 'parent', 'viewport']), maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), minWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), flip: PropTypes.bool, fixed: PropTypes.bool, placement: PropTypes.oneOf([ 'auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start', ]), dynamicWidth: PropTypes.bool, }), /** Callback predicate for the filtering options function */ predicate: PropTypes.func, /** Optional list of strings that are selected suggestions. */ tags: PropTypes.array, /** Max number of visible lines */ maxNumRows: PropTypes.number, /** Delimiters that will trigger a Submit action (call to onTagsAdded). By default it is [,] but also enter and tab keys work. */ delimiters: PropTypes.array, /** Defines a message to be displayed instead of options when no options exist or no options pass the predicate filter function. */ emptyStateMessage: PropTypes.node, /** Specifies whether there are more items to be loaded. */ hasMore: PropTypes.bool, /** Specifies whether lazy loading of the dropdown layout items is enabled. */ infiniteScroll: PropTypes.bool, /** Defines a callback function which is called on a request to render more list items. */ loadMore: PropTypes.func, /** Passing 'select' will render a readOnly input with menuArrow suffix **/ mode: PropTypes.string, /** The status of the Multiselect */ status: PropTypes.oneOf(['loading', 'warning', 'error']), /** Text to be shown in the status icon tooltip */ statusMessage: PropTypes.string, /** When this callback function is set, tags can be reordered. The expected callback signature is `onReorder({addedIndex: number, removedIndex: number}) => void` **/ onReorder: PropTypes.func, /** A callback which is called when the user enters something in the input and then confirms the input with some action like Enter key or Tab. */ onManuallyInput: PropTypes.func, /** A callback which is called when options dropdown is shown */ onOptionsShow: PropTypes.func, /** A callback which is called when options dropdown is hidden */ onOptionsHide: PropTypes.func, /** A callback which is called when the user selects an option from the list. `onSelect(option: Option): void` - Option is the original option from the provided options prop. */ onSelect: PropTypes.func, /** A node to display as input suffix when the dropdown is closed */ customSuffix: PropTypes.node, /** When set to true this component is disabled */ disabled: PropTypes.bool, /** When set to false, the input will not be cleared on blur */ clearOnBlur: PropTypes.bool, /** When set to true, the input will be submitted as new tag on blur */ acceptOnBlur: PropTypes.bool, /** A callback function to be called when a tag should be removed. The expected callback signature is `onRemoveTag(tagId: number | string) => void.` */ onRemoveTag: PropTypes.func, /** Specifies whether input should be read only */ readOnly: PropTypes.bool, /** Adds a fixed footer container at the bottom of options list. */ fixedFooter: PropTypes.node, /** Sets the default option focus behavior: * - `false` - no initially focused list item * - `true` - focus first selectable option * - any `number/string` specify the id of an option to be focused */ markedOption: PropTypes.oneOfType([ PropTypes.bool, PropTypes.string, PropTypes.number, ]), }; MultiSelect.defaultProps = { ...InputWithOptions.defaultProps, predicate: () => true, tags: [], delimiters: [','], clearOnBlur: true, customInput: MultiSelect.autoSizeInputWithRef(), }; export default MultiSelect; //# sourceMappingURL=MultiSelect.js.map