wix-style-react
Version:
wix-style-react
278 lines • 11.5 kB
JavaScript
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