// Simple Dropdown/Autocomplete with single/multiple selection and easy customisation via CSS modules // Version 2021-09-18 // License: LGPLv3.0+ // (c) Vitaliy Filippov 2019+ import React from 'react'; import PropTypes from 'prop-types'; import autocomplete_css from './autocomplete.css'; import PickerMenu from './PickerMenu.js'; export default class Selectbox extends React.PureComponent { static propTypes = { // multi-select multiple: PropTypes.bool, // make text input readonly (disable user input). still allows value change readOnly: PropTypes.bool, // show "clear" icon (cross) allowClear: PropTypes.bool, // select/autocomplete options - either an array of objects, or a { [string]: string } object source: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), // current value value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]), // change callback onChange: PropTypes.func, // do not hide suggestion list on change keepFocusOnChange: PropTypes.bool, // item name key - default "name" labelKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // item text key - default "name" textKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // item id key - default "id" valueKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // item "disabled" key - default none disabledKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // item CSS class key - default none classKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // automatically filter autocomplete options based on user input if `true` suggestionMatch: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['disabled'])]), // additional message to display below autocomplete options (arbitrary HTML, for example "...") suggestionMsg: PropTypes.any, // disable the whole input disabled: PropTypes.bool, // placeholder to display when the input is empty placeholder: PropTypes.string, // minimum suggestion list width in pixels minWidth: PropTypes.number, // change theme (CSS module) for this input theme: PropTypes.object, // additional CSS class name for the input className: PropTypes.string, // additional CSS styles for the input style: PropTypes.object, // additional event listener for onFocus onFocus: PropTypes.func, // additional event listener for onBlur onBlur: PropTypes.func, // additional event listener for user text input onQueryChange: PropTypes.func, } state = { shown: false, query: null, inputWidth: 20, } setQuery = (ev) => { const query = ev.target.value; this.setState({ query }); const f = this.props.onQueryChange; if (f) { f(query); } if (!query.length && !this.props.multiple && this.props.allowClear) { this.clear(); } } clear = () => { this.setState({ query: null }); const f = this.props.onChange; f && f(null); } removeValue = (ev) => { const n = ev.currentTarget.getAttribute('data-n'); if (n != null) { const v = [ ...this.props.value ]; v.splice(n, 1); const f = this.props.onChange; f && f(v); } } onSelectItem = (item) => { this.setState({ query: null }); if (this.props.keepFocusOnChange === false || this.props.keepFocusOnChange == null && !this.props.multiple) { this.picker.blur(); } const sel = item[this.props.valueKey||'id']; if (this.props.disabledKey && item[this.props.disabledKey]) { // item is disabled return; } let value = sel; if (this.props.multiple) { const already = (this.props.value||[]).indexOf(sel); if (already < 0) { // add value = [ ...(this.props.value||[]), sel ]; } else { // remove value = [ ...this.props.value ]; value.splice(already, 1); } } const f = this.props.onChange; f && f(value); } focusInput = () => { if (!this.props.disabled) { this.input.focus(); } } onFocus = () => { if (this.props.disabled) { return; } if (!this.props.multiple && this.picker.state.active === null) { const v = this.props.value, vk = this.props.valueKey||'id'; for (let i = 0; i < this.filtered_items.length; i++) { if (v == this.filtered_items[i][vk]) { this.picker.setState({ active: i }); break; } } } this.picker.focus(); const f = this.props.onFocus; f && f(); } onBlur = () => { this.picker.blur(); const f = this.props.onBlur; f && f(); if (!this.props.multiple && !this.props.allowClear && !this.input.value.length) { this.setState({ query: null }); } } renderInput = (p) => { const theme = this.props.theme || autocomplete_css; const value = this.state.query == null ? (this.props.multiple ? '' : this.item_hash[this.props.value]||'') : this.state.query; const input = ; return (
{this.props.multiple ?
{this.props.multiple && (!this.props.value || !this.props.value.length) && value === '' ? (this.props.placeholder || '') : value} {(this.props.value||[]).map((id, idx) => {this.props.allowClear && !this.props.disabled ? : null} {this.item_hash[id]} )} {input}
: input} {this.props.allowClear && !this.props.multiple && this.props.value != null ?
: null}
); } setSizer = (e) => { this.sizer = e; } setPicker = (e) => { this.picker = e; } setInput = (e) => { this.input = e; } render() { if (!this.prevProps || this.props.source != this.prevProps.source) { if (!this.props.source) { this.items = []; this.item_hash = {}; } else if (this.props.source instanceof Array) { this.items = this.props.source; this.item_hash = this.items.reduce((a, c) => { a[c[this.props.valueKey||'id']] = c[this.props.textKey||'name']; return a; }, {}); } else { this.items = Object.keys(this.props.source).map(id => ({ id, name: this.props.source[id] })); this.item_hash = this.props.source; } } if (!this.prevProps || this.props.source != this.prevProps.source || this.props.suggestionMatch != this.prevProps.suggestionMatch || this.state.query != this.prevState.query) { if (!this.props.suggestionMatch || this.props.suggestionMatch == 'disabled' || !this.state.query) { this.filtered_items = this.items; } else { const n = this.props.textKey||'name'; const q = this.state.query.toLowerCase(); this.filtered_items = this.items.filter(e => e[n].toLowerCase().indexOf(q) >= 0); } } this.prevProps = this.props; this.prevState = this.state; return (); } componentDidUpdate() { if (this.sizer) { this.setState({ inputWidth: this.sizer.offsetWidth }); } } componentDidMount() { this.componentDidUpdate(); } }