Extract drop-down menu code into a separate class
parent
ca689129de
commit
49061e7f1b
22
Picker.js
22
Picker.js
|
@ -4,7 +4,7 @@
|
|||
// ...Or maybe a button with a popup menu
|
||||
// License: LGPLv3.0+
|
||||
// (c) Vitaliy Filippov 2019+
|
||||
// Version 2019-09-03
|
||||
// Version 2020-04-27
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
@ -78,15 +78,25 @@ export default class Picker extends React.Component
|
|||
this.picker = e;
|
||||
}
|
||||
|
||||
render()
|
||||
getInputProps()
|
||||
{
|
||||
return (<React.Fragment>
|
||||
{this.props.renderInput({
|
||||
return {
|
||||
onFocus: this.focus,
|
||||
onBlur: this.blur,
|
||||
focused: this.state.focused,
|
||||
ref: this.setInput,
|
||||
})}
|
||||
};
|
||||
}
|
||||
|
||||
renderPicker()
|
||||
{
|
||||
return this.props.renderPicker();
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return (<React.Fragment>
|
||||
{this.props.renderInput(this.getInputProps())}
|
||||
{this.state.focused
|
||||
? <div style={{
|
||||
position: 'fixed',
|
||||
|
@ -97,7 +107,7 @@ export default class Picker extends React.Component
|
|||
left: this.state.left+'px',
|
||||
zIndex: 100,
|
||||
}} ref={this.setPicker}>
|
||||
{this.props.renderPicker()}
|
||||
{this.renderPicker()}
|
||||
</div>
|
||||
: null}
|
||||
</React.Fragment>);
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
// Menu-like Picker variant with keyboard control
|
||||
// Version 2020-04-27
|
||||
// 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,
|
||||
// menuitem name key - default empty (render the item itself)
|
||||
labelKey: 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
|
||||
this.setState({
|
||||
active: this.state.active == null ? 0 : (
|
||||
(this.state.active + (event.which === 40 ? 1 : this.props.items.length-1)) % this.props.items.length
|
||||
),
|
||||
});
|
||||
if (!this.state.focused)
|
||||
{
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
else if ((ev.which == 10 || ev.which == 13) && this.state.active != null &&
|
||||
this.state.active < this.props.items.length)
|
||||
{
|
||||
// enter
|
||||
this.onMouseDown();
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown = () =>
|
||||
{
|
||||
const sel = this.props.items[this.state.active];
|
||||
const f = this.props.onSelectItem;
|
||||
f && f(sel);
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
animatePicker = (e) =>
|
||||
{
|
||||
if (e)
|
||||
{
|
||||
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.overflowY = 'auto'; e.removeEventListener('transitionend', end); };
|
||||
e.addEventListener('transitionend', end);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderPicker = () =>
|
||||
{
|
||||
const theme = this.props.theme || autocomplete_css;
|
||||
return (<div ref={this.animatePicker}
|
||||
className={theme.suggestions}
|
||||
onMouseOver={this.onMouseOver}>
|
||||
{this.props.beforeItems}
|
||||
{this.props.items.map((e, i) => (<div key={i} id={i} onMouseDown={this.onMouseDown}
|
||||
className={theme.suggestion+(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)
|
||||
{
|
||||
if (this.prevHeight && this.input.offsetHeight != this.prevHeight)
|
||||
{
|
||||
this.calculateDirection();
|
||||
}
|
||||
this.prevHeight = this.input.offsetHeight;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
delete PickerMenu.propTypes.renderPicker;
|
98
Selectbox.js
98
Selectbox.js
|
@ -1,5 +1,5 @@
|
|||
// Simple Dropdown/Autocomplete with single/multiple selection and easy customisation via CSS modules
|
||||
// Version 2019-09-15
|
||||
// Version 2020-04-27
|
||||
// License: LGPLv3.0+
|
||||
// (c) Vitaliy Filippov 2019+
|
||||
|
||||
|
@ -7,7 +7,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import autocomplete_css from './autocomplete.css';
|
||||
import Picker from './Picker.js';
|
||||
import PickerMenu from './PickerMenu.js';
|
||||
|
||||
export default class Selectbox extends React.PureComponent
|
||||
{
|
||||
|
@ -54,7 +54,6 @@ export default class Selectbox extends React.PureComponent
|
|||
|
||||
state = {
|
||||
shown: false,
|
||||
active: null,
|
||||
query: null,
|
||||
inputWidth: 20,
|
||||
}
|
||||
|
@ -74,29 +73,6 @@ export default class Selectbox extends React.PureComponent
|
|||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
clear = () =>
|
||||
{
|
||||
this.setState({ query: null });
|
||||
|
@ -116,11 +92,11 @@ export default class Selectbox extends React.PureComponent
|
|||
}
|
||||
}
|
||||
|
||||
onMouseDown = () =>
|
||||
onSelectItem = (item) =>
|
||||
{
|
||||
this.setState({ query: null });
|
||||
this.picker.blur();
|
||||
const sel = this.filtered_items[this.state.active][this.props.valueKey||'id'];
|
||||
const sel = item[this.props.valueKey||'id'];
|
||||
let value = sel;
|
||||
if (this.props.multiple)
|
||||
{
|
||||
|
@ -141,19 +117,6 @@ export default class Selectbox extends React.PureComponent
|
|||
f && f(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 = () =>
|
||||
{
|
||||
if (!this.props.disabled)
|
||||
|
@ -168,14 +131,14 @@ export default class Selectbox extends React.PureComponent
|
|||
{
|
||||
return;
|
||||
}
|
||||
if (!this.props.multiple && this.state.active === null)
|
||||
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.setState({ active: i });
|
||||
this.picker.setState({ active: i });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -213,7 +176,7 @@ export default class Selectbox extends React.PureComponent
|
|||
onBlur={this.onBlur}
|
||||
value={value}
|
||||
onChange={this.setQuery}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyDown={p.onKeyDown}
|
||||
/>;
|
||||
return (<div ref={p.ref}
|
||||
className={theme.input +
|
||||
|
@ -247,41 +210,6 @@ export default class Selectbox extends React.PureComponent
|
|||
</div>);
|
||||
}
|
||||
|
||||
animateSuggestions = (e) =>
|
||||
{
|
||||
if (e)
|
||||
{
|
||||
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.overflowY = 'auto'; e.removeEventListener('transitionend', end); };
|
||||
e.addEventListener('transitionend', end);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderSuggestions = () =>
|
||||
{
|
||||
const theme = this.props.theme || autocomplete_css;
|
||||
return (<div ref={this.animateSuggestions}
|
||||
className={theme.suggestions}
|
||||
onMouseOver={this.onMouseOver}>
|
||||
{this.filtered_items.map((e, i) => (<div key={i} id={i} onMouseDown={this.onMouseDown}
|
||||
className={theme.suggestion+(this.state.active == i ? ' '+theme.active : '')}>
|
||||
{e[this.props.labelKey||'name']}
|
||||
</div>))}
|
||||
{this.props.suggestionMsg}
|
||||
</div>);
|
||||
}
|
||||
|
||||
setSizer = (e) =>
|
||||
{
|
||||
this.sizer = e;
|
||||
|
@ -334,12 +262,15 @@ export default class Selectbox extends React.PureComponent
|
|||
}
|
||||
this.prevProps = this.props;
|
||||
this.prevState = this.state;
|
||||
return (<Picker
|
||||
return (<PickerMenu
|
||||
ref={this.setPicker}
|
||||
minWidth={this.props.minWidth}
|
||||
clearOnClick={true}
|
||||
renderInput={this.renderInput}
|
||||
renderPicker={this.renderSuggestions}
|
||||
items={this.filtered_items}
|
||||
labelKey={this.props.labelKey||'name'}
|
||||
afterItems={this.props.suggestionMsg}
|
||||
onSelectItem={this.onSelectItem}
|
||||
/>);
|
||||
}
|
||||
|
||||
|
@ -348,11 +279,6 @@ export default class Selectbox extends React.PureComponent
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/* extjs-like <button> */
|
||||
|
||||
.button
|
||||
{
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
border: 1px solid #d8d8d8;
|
||||
outline: none;
|
||||
position: relative;
|
||||
background: #f5f5f5;
|
||||
border-radius: 3px;
|
||||
font-size: inherit;
|
||||
line-height: 2em;
|
||||
padding: 0 .6em;
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
.button::-moz-focus-inner
|
||||
{
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.button:focus:after
|
||||
{
|
||||
content: " ";
|
||||
display: block;
|
||||
border: 1px solid #4196d4;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.button:not([disabled]):hover
|
||||
{
|
||||
border: 1px solid #cfcfcf;
|
||||
background: #ebebeb;
|
||||
}
|
||||
|
||||
.button:not([disabled]):active, .button.pressed
|
||||
{
|
||||
border: 1px solid #c6c6c6;
|
||||
background: #e1e1e1;
|
||||
}
|
||||
|
||||
.button[disabled]
|
||||
{
|
||||
color: #c0c0c0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.button :global(span.material-icons)
|
||||
{
|
||||
font-size: inherit;
|
||||
line-height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.button.blue
|
||||
{
|
||||
background: #3892d3;
|
||||
border-color: #157fcc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button.blue:focus:after
|
||||
{
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
.button.blue:not([disabled]):hover
|
||||
{
|
||||
border: 1px solid #1374bb;
|
||||
background: #3386c2;
|
||||
}
|
||||
|
||||
.button.blue:not([disabled]):active, .button.blue.pressed
|
||||
{
|
||||
border: 1px solid #0f5f99;
|
||||
background: #2a6d9e;
|
||||
}
|
||||
|
||||
.button.blue[disabled]
|
||||
{
|
||||
color: white;
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
12
main.js
12
main.js
|
@ -2,6 +2,8 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
|
||||
import Selectbox from './Selectbox.js';
|
||||
import PickerMenu from './PickerMenu.js';
|
||||
import button_css from './button.css';
|
||||
|
||||
const OPTIONS = {
|
||||
day: 'День',
|
||||
|
@ -10,6 +12,8 @@ const OPTIONS = {
|
|||
kak: 'Полный период детализации',
|
||||
};
|
||||
|
||||
const NAMES = Object.values(OPTIONS);
|
||||
|
||||
class Test extends React.PureComponent
|
||||
{
|
||||
state = {
|
||||
|
@ -29,7 +33,7 @@ class Test extends React.PureComponent
|
|||
|
||||
render()
|
||||
{
|
||||
return <div style={{padding: '20px', width: '300px', background: '#e0e8ff'}}>
|
||||
return <div style={{padding: '20px', width: '300px', background: '#e0e8ff', fontSize: '13px'}}>
|
||||
<Selectbox
|
||||
source={OPTIONS}
|
||||
allowClear={true}
|
||||
|
@ -66,8 +70,14 @@ class Test extends React.PureComponent
|
|||
multiple={false}
|
||||
suggestionMatch={true}
|
||||
value={this.state.value2}
|
||||
style={{marginBottom: '20px'}}
|
||||
onChange={this.onChange2}
|
||||
/>
|
||||
<PickerMenu
|
||||
clearOnClick={true}
|
||||
renderInput={p => <button {...p} className={button_css.button}>Меню</button>}
|
||||
items={NAMES}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue