Extract drop-down menu code into a separate class
parent
ca689129de
commit
49061e7f1b
26
Picker.js
26
Picker.js
|
@ -4,7 +4,7 @@
|
||||||
// ...Or maybe a button with a popup menu
|
// ...Or maybe a button with a popup menu
|
||||||
// License: LGPLv3.0+
|
// License: LGPLv3.0+
|
||||||
// (c) Vitaliy Filippov 2019+
|
// (c) Vitaliy Filippov 2019+
|
||||||
// Version 2019-09-03
|
// Version 2020-04-27
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
@ -78,15 +78,25 @@ export default class Picker extends React.Component
|
||||||
this.picker = e;
|
this.picker = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInputProps()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
onFocus: this.focus,
|
||||||
|
onBlur: this.blur,
|
||||||
|
focused: this.state.focused,
|
||||||
|
ref: this.setInput,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPicker()
|
||||||
|
{
|
||||||
|
return this.props.renderPicker();
|
||||||
|
}
|
||||||
|
|
||||||
render()
|
render()
|
||||||
{
|
{
|
||||||
return (<React.Fragment>
|
return (<React.Fragment>
|
||||||
{this.props.renderInput({
|
{this.props.renderInput(this.getInputProps())}
|
||||||
onFocus: this.focus,
|
|
||||||
onBlur: this.blur,
|
|
||||||
focused: this.state.focused,
|
|
||||||
ref: this.setInput,
|
|
||||||
})}
|
|
||||||
{this.state.focused
|
{this.state.focused
|
||||||
? <div style={{
|
? <div style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
|
@ -97,7 +107,7 @@ export default class Picker extends React.Component
|
||||||
left: this.state.left+'px',
|
left: this.state.left+'px',
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
}} ref={this.setPicker}>
|
}} ref={this.setPicker}>
|
||||||
{this.props.renderPicker()}
|
{this.renderPicker()}
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
</React.Fragment>);
|
</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
|
// Simple Dropdown/Autocomplete with single/multiple selection and easy customisation via CSS modules
|
||||||
// Version 2019-09-15
|
// Version 2020-04-27
|
||||||
// License: LGPLv3.0+
|
// License: LGPLv3.0+
|
||||||
// (c) Vitaliy Filippov 2019+
|
// (c) Vitaliy Filippov 2019+
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import autocomplete_css from './autocomplete.css';
|
import autocomplete_css from './autocomplete.css';
|
||||||
import Picker from './Picker.js';
|
import PickerMenu from './PickerMenu.js';
|
||||||
|
|
||||||
export default class Selectbox extends React.PureComponent
|
export default class Selectbox extends React.PureComponent
|
||||||
{
|
{
|
||||||
|
@ -54,7 +54,6 @@ export default class Selectbox extends React.PureComponent
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
shown: false,
|
shown: false,
|
||||||
active: null,
|
|
||||||
query: null,
|
query: null,
|
||||||
inputWidth: 20,
|
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 = () =>
|
clear = () =>
|
||||||
{
|
{
|
||||||
this.setState({ query: null });
|
this.setState({ query: null });
|
||||||
|
@ -116,11 +92,11 @@ export default class Selectbox extends React.PureComponent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseDown = () =>
|
onSelectItem = (item) =>
|
||||||
{
|
{
|
||||||
this.setState({ query: null });
|
this.setState({ query: null });
|
||||||
this.picker.blur();
|
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;
|
let value = sel;
|
||||||
if (this.props.multiple)
|
if (this.props.multiple)
|
||||||
{
|
{
|
||||||
|
@ -141,19 +117,6 @@ export default class Selectbox extends React.PureComponent
|
||||||
f && f(value);
|
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 = () =>
|
focusInput = () =>
|
||||||
{
|
{
|
||||||
if (!this.props.disabled)
|
if (!this.props.disabled)
|
||||||
|
@ -168,14 +131,14 @@ export default class Selectbox extends React.PureComponent
|
||||||
{
|
{
|
||||||
return;
|
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';
|
const v = this.props.value, vk = this.props.valueKey||'id';
|
||||||
for (let i = 0; i < this.filtered_items.length; i++)
|
for (let i = 0; i < this.filtered_items.length; i++)
|
||||||
{
|
{
|
||||||
if (v == this.filtered_items[i][vk])
|
if (v == this.filtered_items[i][vk])
|
||||||
{
|
{
|
||||||
this.setState({ active: i });
|
this.picker.setState({ active: i });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,7 +176,7 @@ export default class Selectbox extends React.PureComponent
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.setQuery}
|
onChange={this.setQuery}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={p.onKeyDown}
|
||||||
/>;
|
/>;
|
||||||
return (<div ref={p.ref}
|
return (<div ref={p.ref}
|
||||||
className={theme.input +
|
className={theme.input +
|
||||||
|
@ -247,41 +210,6 @@ export default class Selectbox extends React.PureComponent
|
||||||
</div>);
|
</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) =>
|
setSizer = (e) =>
|
||||||
{
|
{
|
||||||
this.sizer = e;
|
this.sizer = e;
|
||||||
|
@ -334,12 +262,15 @@ export default class Selectbox extends React.PureComponent
|
||||||
}
|
}
|
||||||
this.prevProps = this.props;
|
this.prevProps = this.props;
|
||||||
this.prevState = this.state;
|
this.prevState = this.state;
|
||||||
return (<Picker
|
return (<PickerMenu
|
||||||
ref={this.setPicker}
|
ref={this.setPicker}
|
||||||
minWidth={this.props.minWidth}
|
minWidth={this.props.minWidth}
|
||||||
clearOnClick={true}
|
clearOnClick={true}
|
||||||
renderInput={this.renderInput}
|
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)
|
if (this.sizer)
|
||||||
{
|
{
|
||||||
this.setState({ inputWidth: this.sizer.offsetWidth });
|
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 ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import Selectbox from './Selectbox.js';
|
import Selectbox from './Selectbox.js';
|
||||||
|
import PickerMenu from './PickerMenu.js';
|
||||||
|
import button_css from './button.css';
|
||||||
|
|
||||||
const OPTIONS = {
|
const OPTIONS = {
|
||||||
day: 'День',
|
day: 'День',
|
||||||
|
@ -10,6 +12,8 @@ const OPTIONS = {
|
||||||
kak: 'Полный период детализации',
|
kak: 'Полный период детализации',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NAMES = Object.values(OPTIONS);
|
||||||
|
|
||||||
class Test extends React.PureComponent
|
class Test extends React.PureComponent
|
||||||
{
|
{
|
||||||
state = {
|
state = {
|
||||||
|
@ -29,7 +33,7 @@ class Test extends React.PureComponent
|
||||||
|
|
||||||
render()
|
render()
|
||||||
{
|
{
|
||||||
return <div style={{padding: '20px', width: '300px', background: '#e0e8ff'}}>
|
return <div style={{padding: '20px', width: '300px', background: '#e0e8ff', fontSize: '13px'}}>
|
||||||
<Selectbox
|
<Selectbox
|
||||||
source={OPTIONS}
|
source={OPTIONS}
|
||||||
allowClear={true}
|
allowClear={true}
|
||||||
|
@ -66,8 +70,14 @@ class Test extends React.PureComponent
|
||||||
multiple={false}
|
multiple={false}
|
||||||
suggestionMatch={true}
|
suggestionMatch={true}
|
||||||
value={this.state.value2}
|
value={this.state.value2}
|
||||||
|
style={{marginBottom: '20px'}}
|
||||||
onChange={this.onChange2}
|
onChange={this.onChange2}
|
||||||
/>
|
/>
|
||||||
|
<PickerMenu
|
||||||
|
clearOnClick={true}
|
||||||
|
renderInput={p => <button {...p} className={button_css.button}>Меню</button>}
|
||||||
|
items={NAMES}
|
||||||
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue