customized-multiselect-dropdown
Version:
React multiselect custom dropdown component with search and various features
577 lines (544 loc) • 17.5 kB
JavaScript
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,
};