selectbox/Selectbox.js

208 lines
6.6 KiB
JavaScript

// Simple Dropdown/Autocomplete with single/multiple selection and easy customisation via CSS modules
// Version 2019-08-27
// (c) Vitaliy Filippov 2019+
import React from 'react';
import PropTypes from 'prop-types';
import autocomplete_css from './autocomplete.css';
import input_css from './input.css';
import { Picker } from './Picker.js';
export default class Selectbox extends React.PureComponent
{
state = {
active: null,
query: null,
inputWidth: 20,
}
setQuery = (ev) =>
{
this.setState({ query: ev.target.value });
}
onKeyDown = (ev) =>
{
if ((ev.which == 40 || ev.which == 38) && this.filtered_items.length)
{
// up / down
this.setState({
active: this.state.active == null ? 0 : (
(this.state.active + (event.which === 40 ? 1 : this.filtered_items.length-1)) % this.filtered_items.length
),
});
if (!this.picker.state.focused)
{
this.picker.focus();
}
}
else if ((ev.which == 10 || ev.which == 13) && this.state.active != null &&
this.state.active < this.filtered_items.length)
{
// enter
this.onMouseDown();
}
}
onMouseDown = () =>
{
this.setState({ query: null });
this.picker.blur();
const sel = this.filtered_items[this.state.active][this.props.valueKey||'id'];
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);
}
}
this.props.onChange(value);
}
onMouseOver = (ev) =>
{
let e = ev.target;
while (e && e != ev.currentTarget && !e.id)
{
e = e.parentNode;
}
if (e && e.id)
{
this.setState({ active: e.id });
}
}
focusInput = () =>
{
this.input.focus();
}
renderInput = (p) =>
{
const value = this.state.query == null ? (this.props.multiple ? '' : this.item_hash[this.props.value]||'') : this.state.query;
const input = <input
readOnly={this.props.readOnly}
ref={this.setInput}
className={autocomplete_css.inputInputElement}
style={this.props.multiple ? { height: '29px', padding: 0, display: 'inline-block', width: this.state.inputWidth+'px' } : undefined}
onFocus={p.onFocus}
value={value}
onChange={this.setQuery}
onKeyDown={this.onKeyDown}
/>;
return (<div ref={p.ref} className={autocomplete_css.input}
style={this.props.multiple ? { fontSize: '13px', display: 'flex', overflow: 'hidden', cursor: 'text', height: 'auto' } : undefined}
onClick={this.props.multiple ? this.focusInput : undefined}>
{this.props.multiple
? <div style={{overflow: 'hidden', flex: 1, margin: '0 2em 0 .5em'}}>
<span className={autocomplete_css.inputInputElement} ref={this.setSizer}
style={{display: 'inline-block', width: 'auto', position: 'absolute', padding: '0 2px', top: '-100px', whiteSpace: 'pre', visibility: 'hidden'}}>
{value}
</span>
{(this.props.value||[]).map(id => <span style={{cursor: 'pointer', display: 'inline-block', borderRadius: '2px', background: '#e0f0ff', border: '1px solid #c0e0ff', padding: '3px', margin: '2px 5px 2px 0'}}>{this.item_hash[id]}</span>)}
{input}
</div>
: input}
</div>);
}
renderSuggestions = () =>
{
return (<div className={autocomplete_css.suggestions} onMouseDown={this.onMouseDown} onMouseOver={this.onMouseOver}>
{this.filtered_items.map((e, i) => (<div key={i} id={i}
className={autocomplete_css.suggestion+(this.state.active == i ? ' '+autocomplete_css.active : '')}>
{e[this.props.labelKey||'name']}
</div>))}
</div>);
}
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.labelKey||'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.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.labelKey||'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 (<Picker
ref={this.setPicker}
minWidth={this.props.minWidth}
clearOnClick={true}
renderInput={this.renderInput}
renderPicker={this.renderSuggestions}
style={this.props.style}
/>);
}
componentDidUpdate()
{
if (this.sizer)
{
this.setState({ inputWidth: this.sizer.offsetWidth });
if (this.prevHeight && this.picker.input.offsetHeight != this.prevHeight)
{
this.picker.calculateDirection();
}
this.prevHeight = this.picker.input.offsetHeight;
}
}
componentDidMount()
{
this.componentDidUpdate();
}
}