UNPKG

customized-multiselect-dropdown

Version:

React multiselect custom dropdown component with search and various features

577 lines (544 loc) 17.5 kB
import React from "react"; import ms from "./multiselect.component.css"; import "../assets/closeicon/css/fontello.css"; const closeIconTypes = { circle: "icon_cancel_circled", circle2: "icon_cancel_circled2", close: "icon_window_close", cancel: "icon_cancel", }; export class Multiselect extends React.Component { constructor(props) { super(props); this.state = { inputValue: "", options: props.options, filteredOptions: props.options, unfilteredOptions: props.options, selectedValues: Object.assign([], props.selectedValues), preSelectedValues: Object.assign([], props.selectedValues), toggleOptionsList: false, highlightOption: props.avoidHighlightFirstOption ? -1 : 0, showCheckbox: props.showCheckbox, groupedObject: [], closeIconType: closeIconTypes[props.closeIcon] || closeIconTypes["circle"], }; this.searchWrapper = React.createRef(); this.searchBox = React.createRef(); this.onChange = this.onChange.bind(this); this.renderMultiselectContainer = this.renderMultiselectContainer.bind( this ); this.renderSelectedList = this.renderSelectedList.bind(this); this.onRemoveSelectedItem = this.onRemoveSelectedItem.bind(this); this.toggelOptionList = this.toggelOptionList.bind(this); this.onArrowKeyNavigation = this.onArrowKeyNavigation.bind(this); this.onSelectItem = this.onSelectItem.bind(this); this.filterOptionsByInput = this.filterOptionsByInput.bind(this); this.removeSelectedValuesFromOptions = this.removeSelectedValuesFromOptions.bind( this ); this.isSelectedValue = this.isSelectedValue.bind(this); this.fadeOutSelection = this.fadeOutSelection.bind(this); this.isDisablePreSelectedValues = this.isDisablePreSelectedValues.bind( this ); this.renderGroupByOptions = this.renderGroupByOptions.bind(this); this.renderNormalOption = this.renderNormalOption.bind(this); this.listenerCallback = this.listenerCallback.bind(this); this.resetSelectedValues = this.resetSelectedValues.bind(this); this.getSelectedItems = this.getSelectedItems.bind(this); this.getSelectedItemsCount = this.getSelectedItemsCount.bind(this); } initialSetValue() { const { showCheckbox, groupBy, singleSelect } = this.props; const { options } = this.state; if (!showCheckbox && !singleSelect) { this.removeSelectedValuesFromOptions(false); } if (groupBy && showCheckbox) { this.groupByOptions(options); } } resetSelectedValues() { const { unfilteredOptions } = this.state; this.setState( { selectedValues: [], preSelectedValues: [], options: unfilteredOptions, filteredOptions: unfilteredOptions, }, this.initialSetValue ); } getSelectedItems() { return this.state.selectedValues; } getSelectedItemsCount() { return this.state.selectedValues.length; } componentDidMount() { this.initialSetValue(); this.searchWrapper.current.addEventListener("click", this.listenerCallback); } componentDidUpdate(prevProps) { const { options, selectedValues } = this.props; const { options: prevOptions, selectedValues: prevSelectedvalues, } = prevProps; if (JSON.stringify(prevOptions) !== JSON.stringify(options)) { this.setState( { options, filteredOptions: options, unfilteredOptions: options }, this.initialSetValue ); } if (JSON.stringify(prevSelectedvalues) !== JSON.stringify(selectedValues)) { this.setState( { selectedValues: Object.assign([], selectedValues), preSelectedValues: Object.assign([], selectedValues), }, this.initialSetValue ); } } listenerCallback() { this.searchBox.current.focus(); } componentWillUnmount() { this.searchWrapper.current.removeEventListener( "click", this.listenerCallback ); } // Skipcheck flag - value will be true when the func called from on deselect anything. removeSelectedValuesFromOptions(skipCheck) { const { isObject, displayValue, groupBy } = this.props; const { selectedValues = [], unfilteredOptions, options } = this.state; if (!skipCheck && groupBy) { this.groupByOptions(options); } if (!selectedValues.length && !skipCheck) { return; } if (isObject) { let optionList = unfilteredOptions.filter((item) => { return selectedValues.findIndex( (v) => v[displayValue] === item[displayValue] ) === -1 ? true : false; }); if (groupBy) { this.groupByOptions(optionList); } this.setState( { options: optionList, filteredOptions: optionList }, this.filterOptionsByInput ); return; } let optionList = unfilteredOptions.filter( (item) => selectedValues.indexOf(item) === -1 ); this.setState( { options: optionList, filteredOptions: optionList }, this.filterOptionsByInput ); } groupByOptions(options) { const { groupBy } = this.props; const groupedObject = options.reduce(function (r, a) { const key = a[groupBy] || "Others"; r[key] = r[key] || []; r[key].push(a); return r; }, Object.create({})); this.setState({ groupedObject }); } onChange(event) { this.setState( { inputValue: event.target.value }, this.filterOptionsByInput ); } filterOptionsByInput() { let { options, filteredOptions, inputValue } = this.state; const { isObject, displayValue } = this.props; if (isObject) { options = filteredOptions.filter((i) => this.matchValues(i[displayValue], inputValue) ); } else { options = filteredOptions.filter((i) => this.matchValues(i, inputValue)); } this.groupByOptions(options); this.setState({ options }); } matchValues(value, search) { if (this.props.caseSensitiveSearch) { return value.indexOf(search) > -1; } return value.toLowerCase().indexOf(search.toLowerCase()) > -1; } onArrowKeyNavigation(e) { const { options, highlightOption, toggleOptionsList, inputValue, selectedValues, } = this.state; if (e.keyCode === 8 && !inputValue && selectedValues.length) { this.onRemoveSelectedItem(selectedValues.length - 1); } if (!options.length) { return; } if (e.keyCode === 38) { if (highlightOption > 0) { this.setState((previousState) => ({ highlightOption: previousState.highlightOption - 1, })); } else { this.setState({ highlightOption: options.length - 1 }); } } else if (e.keyCode === 40) { if (highlightOption < options.length - 1) { this.setState((previousState) => ({ highlightOption: previousState.highlightOption + 1, })); } else { this.setState({ highlightOption: 0 }); } } else if (e.key === "Enter" && options.length && toggleOptionsList) { if (highlightOption === -1) { return; } this.onSelectItem(options[highlightOption]); } // TODO: Instead of scrollIntoView need to find better soln for scroll the dropwdown container. // setTimeout(() => { // const element = document.querySelector("ul.optionContainer .highlight"); // if (element) { // element.scrollIntoView(); // } // }); } onRemoveSelectedItem(item) { let { selectedValues, index = 0, isObject } = this.state; const { onRemove, showCheckbox } = this.props; if (isObject) { index = selectedValues.findIndex( (i) => i[displayValue] === item[displayValue] ); } else { index = selectedValues.indexOf(item); } selectedValues.splice(index, 1); onRemove(selectedValues, item); this.setState({ selectedValues }, () => { if (!showCheckbox) { this.removeSelectedValuesFromOptions(true); } }); } onSelectItem(item) { const { selectedValues } = this.state; const { selectionLimit, onSelect, singleSelect, showCheckbox } = this.props; this.setState({ inputValue: "", }); if (singleSelect) { this.onSingleSelect(item); onSelect([item], item); return; } if (this.isSelectedValue(item)) { this.onRemoveSelectedItem(item); return; } if (selectionLimit == selectedValues.length) { return; } selectedValues.push(item); onSelect(selectedValues, item); this.setState({ selectedValues }, () => { if (!showCheckbox) { this.removeSelectedValuesFromOptions(true); } else { this.filterOptionsByInput(); } }); if (!this.props.closeOnSelect) { this.searchBox.current.focus(); } } onSingleSelect(item) { this.setState({ selectedValues: [item], toggleOptionsList: false }); } isSelectedValue(item) { const { isObject, displayValue } = this.props; const { selectedValues } = this.state; if (isObject) { return ( selectedValues.filter((i) => i[displayValue] === item[displayValue]) .length > 0 ); } return selectedValues.filter((i) => i === item).length > 0; } renderOptionList() { const { groupBy, style, emptyRecordMsg } = this.props; const { options } = this.state; return ( <ul className={`optionContainer`} style={style["optionContainer"]}> {options.length === 0 && ( <span style={style["notFound"]} className={`notFound ${ms.notFound}`}> {emptyRecordMsg} </span> )} {!groupBy ? this.renderNormalOption() : this.renderGroupByOptions()} </ul> ); } renderGroupByOptions() { const { isObject = false, displayValue, showCheckbox, style, singleSelect, } = this.props; const { groupedObject } = this.state; return Object.keys(groupedObject).map((obj) => { return ( <React.Fragment key={obj}> <li className={ms.groupHeading} style={style["groupHeading"]}> {obj} </li> {groupedObject[obj].map((option, i) => ( <li key={`option${i}`} style={style["option"]} className={`${ms.groupChildEle} ${ this.fadeOutSelection(option) && ms.disableSelection } option`} onClick={() => this.onSelectItem(option)} > {showCheckbox && !singleSelect && ( <div> <input type="checkbox" class="styled-checkbox" readOnly checked={this.isSelectedValue(option)} /> <label for="styled-checkbox"> {isObject ? option[displayValue] : (option || "").toString()} </label> </div> )} </li> ))} </React.Fragment> ); }); } renderNormalOption() { const { isObject = false, displayValue, showCheckbox, style, singleSelect, } = this.props; const { highlightOption } = this.state; return this.state.options.map((option, i) => ( <li key={`option${i}`} style={style["option"]} className={`${ highlightOption === i ? `${ms.highlightOption} highlight` : "" } ${this.fadeOutSelection(option) && ms.disableSelection} option`} onClick={() => this.onSelectItem(option)} > {showCheckbox && !singleSelect && ( <input type="checkbox" readOnly className={`checkbox ${ms.checkbox}`} checked={this.isSelectedValue(option)} /> )} {isObject ? option[displayValue] : (option || "").toString()} </li> )); } renderSelectedList() { const { isObject = false, displayValue, style, singleSelect } = this.props; const { selectedValues, closeIconType } = this.state; return selectedValues.map((value, index) => ( <span className={`chip ${ms.chip} ${singleSelect && ms.singleChip} ${ this.isDisablePreSelectedValues(value) && ms.disableSelection }`} key={index} style={style["chips"]} > {!isObject ? (value || "").toString() : value[displayValue]} <i className={`icon_cancel ${ms[closeIconType]} ${ms.closeIcon}`} onClick={() => this.onRemoveSelectedItem(value)} /> </span> )); } isDisablePreSelectedValues(value) { const { isObject, disablePreSelectedValues, displayValue } = this.props; const { preSelectedValues } = this.state; if (!disablePreSelectedValues || !preSelectedValues.length) { return false; } if (isObject) { return ( preSelectedValues.filter((i) => i[displayValue] === value[displayValue]) .length > 0 ); } return preSelectedValues.filter((i) => i === value).length > 0; } fadeOutSelection(item) { const { selectionLimit, showCheckbox, singleSelect } = this.props; if (singleSelect) { return; } const { selectedValues } = this.state; if (selectionLimit == -1) { return false; } if (selectionLimit != selectedValues.length) { return false; } if (selectionLimit == selectedValues.length) { if (!showCheckbox) { return true; } else { if (this.isSelectedValue(item)) { return false; } return true; } } } toggelOptionList(eventType) { const { singleSelect } = this.props; const { toggleOptionsList } = this.state; if (singleSelect) { this.setState({ toggleOptionsList: !this.state.toggleOptionsList, highlightOption: this.props.avoidHighlightFirstOption ? -1 : 0, }); } else { if (eventType === "onClickEvent") { this.setState({ toggleOptionsList: !toggleOptionsList, highlightOption: this.props.avoidHighlightFirstOption ? -1 : 0, }); } else { this.setState({ highlightOption: this.props.avoidHighlightFirstOption ? -1 : 0, }); } } } renderMultiselectContainer() { const { inputValue, toggleOptionsList, selectedValues } = this.state; const { placeholder, style, singleSelect, id, hidePlaceholder, disable, } = this.props; return ( <div className={`${ms.multiSelectContainer} ${ disable ? `${ms.disable_ms} disable_ms` : "" }`} id={id || "multiselectContainerReact"} style={style["multiselectContainer"]} > <div className={`${ms.searchWrapper} ${ singleSelect ? ms.singleSelect : "" }`} ref={this.searchWrapper} style={style["searchBox"]} onClick={() => this.toggelOptionList("onClickEvent")} > {this.renderSelectedList()} <input type="text" ref={this.searchBox} className="searchBox" id={`${id || "search"}_input`} onChange={this.onChange} value={inputValue} onClick={() => this.toggelOptionList("onClickEvent")} onBlur={() => setTimeout(this.toggelOptionList("onBlurEvent"), 200)} placeholder={ (singleSelect && selectedValues.length) || (hidePlaceholder && selectedValues.length) ? "" : placeholder } onKeyDown={this.onArrowKeyNavigation} style={style["inputField"]} autoComplete="off" disabled={singleSelect || disable} /> {singleSelect && <i className={`icon_cancel ${ms.icon_down_dir}`} />} </div> <div className={`optionListContainer ${ms.optionListContainer} ${ toggleOptionsList ? ms.displayBlock : ms.displayNone }`} > {this.renderOptionList()} </div> </div> ); } render() { return this.renderMultiselectContainer(); } } Multiselect.defaultProps = { options: [], disablePreSelectedValues: false, selectedValues: [], isObject: true, displayValue: "model", showCheckbox: false, selectionLimit: -1, placeholder: "Select", groupBy: "", style: {}, emptyRecordMsg: "No Options Available", onSelect: () => {}, onRemove: () => {}, closeIcon: "circle2", singleSelect: false, caseSensitiveSearch: false, id: "", closeOnSelect: true, avoidHighlightFirstOption: false, hidePlaceholder: false, };