selectbox/PickerMenu.js

192 lines
5.8 KiB
JavaScript

// Menu-like Picker variant with keyboard control
// Version 2021-08-30
// License: LGPLv3.0+
// (c) Vitaliy Filippov 2020+
import React from 'react';
import PropTypes from 'prop-types';
import autocomplete_css from './autocomplete.css';
import Picker from './Picker.js';
export default class PickerMenu extends Picker
{
static propTypes = {
...Picker.propTypes,
// menu options
items: PropTypes.array,
// additional text/items to render before menu items
beforeItems: PropTypes.any,
// additional text/items to render after menu items
afterItems: PropTypes.any,
// menuitem callback
onSelectItem: PropTypes.func,
// don't hide the menu on item click
keepOnClick: PropTypes.bool,
// menuitem name key - default empty (render the item itself)
labelKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// menuitem "disabled" key - default none
disabledKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// change theme (CSS module) for this input
theme: PropTypes.object,
}
onKeyDown = (ev) =>
{
if ((ev.which == 40 || ev.which == 38) && this.props.items.length)
{
// up / down
const dir = (event.which === 40 ? 1 : -1);
let prev = this.state.active, active;
if (prev == null)
active = dir == 1 ? 0 : this.props.items.length-1;
else
active = (prev + this.props.items.length - dir) % this.props.items.length;
if (this.props.disabledKey)
{
while (this.props.items[active][this.props.disabledKey] && active != prev)
{
active = (active + this.props.items.length - dir) % this.props.items.length;
}
if (this.props.items[active][this.props.disabledKey])
{
active = null;
}
}
this.setState({ active });
if (!this.state.focused)
{
this.focus();
}
}
else if (ev.which == 10 || ev.which == 13)
{
// enter
if (!this.state.focused)
{
this.focus();
}
else if (this.state.active != null && this.state.active < this.props.items.length)
{
this.onMouseDown();
}
}
}
onMouseDown = (ev) =>
{
if (!this.props.keepOnClick)
{
if (this.input)
{
this.input.blur();
}
this.blur();
}
else if (ev)
{
ev.preventDefault();
}
const sel = this.props.items[this.state.active];
const f = this.props.onSelectItem;
if (f)
{
f(sel);
}
else if (sel.onClick)
{
sel.onClick();
}
}
onMouseOver = (ev) =>
{
let e = ev.target;
while (e && e != ev.currentTarget && !e.getAttribute('data-idx'))
{
e = e.parentNode;
}
if (e && e.getAttribute('data-idx') &&
(!this.props.disabledKey || !this.props.items[e.getAttribute('data-idx')][this.props.disabledKey]))
{
this.setState({ active: e.getAttribute('data-idx') });
}
}
animatePicker = (e) =>
{
if (e)
{
if (!this.props.renderInput)
{
e.focus();
}
e.style.visibility = 'hidden';
e.style.overflowY = 'hidden';
const anim = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
anim(() =>
{
e.style.visibility = '';
e.style.maxHeight = '1px';
anim(() =>
{
e.style.maxHeight = '100%';
const end = () =>
{
e.style.maxHeight = 'none';
e.style.overflowY = 'auto';
e.removeEventListener('transitionend', end);
};
e.addEventListener('transitionend', end);
});
});
}
}
renderPicker = () =>
{
const theme = this.props.theme || autocomplete_css;
if (!this.props.beforeItems && !this.props.items.length && !this.props.afterItems)
{
return null;
}
return (<div ref={this.animatePicker}
tabIndex={!this.props.renderInput ? 1 : undefined}
className={theme.suggestions}
onKeyDown={this.onKeyDown}
onMouseOver={this.onMouseOver}>
{this.props.beforeItems}
{this.props.items.map((e, i) => (<div key={i} data-idx={i} onMouseDown={this.onMouseDown}
className={theme.suggestion+
(this.props.disabledKey && e[this.props.disabledKey] ? ' '+theme.disabled : '')+
(this.state.active == i ? ' '+theme.active : '')}>
{!this.props.labelKey ? e : e[this.props.labelKey]}
</div>))}
{this.props.afterItems}
</div>);
}
getInputProps()
{
return {
...super.getInputProps(),
onKeyDown: this.onKeyDown,
};
}
componentDidUpdate()
{
super.componentDidUpdate();
if (this.input && this.state.focused)
{
if (this.prevHeight && this.input.offsetHeight != this.prevHeight)
{
this.calculateDirection();
}
this.prevHeight = this.input.offsetHeight;
}
}
}
delete PickerMenu.propTypes.renderPicker;