Add popup (no-input mode) support to Picker

master
Vitaliy Filippov 2020-04-27 18:21:12 +03:00
parent 9544216475
commit 50d82f6fd4
4 changed files with 135 additions and 56 deletions

127
Picker.js
View File

@ -14,12 +14,14 @@ export default class Picker extends React.Component
{
static propTypes = {
direction: PropTypes.string,
clearOnClick: PropTypes.bool,
autoHide: PropTypes.bool,
minWidth: PropTypes.number,
className: PropTypes.string,
style: PropTypes.object,
renderInput: PropTypes.func.isRequired,
renderInput: PropTypes.func,
renderPicker: PropTypes.func.isRequired,
popupX: PropTypes.number,
popupY: PropTypes.number,
onHide: PropTypes.func,
}
state = {
@ -32,23 +34,22 @@ export default class Picker extends React.Component
focus = () =>
{
if (!this.state.focused)
if (!this.state.focused && this.props.renderInput)
{
this.setState({ focused: true, height: 0 });
this.calculateDirection();
if (this.props.clearOnClick)
{
document.body.addEventListener('click', this.blurExt);
}
}
}
blur = () =>
{
this.setState({ focused: false });
if (this.props.clearOnClick)
if (this.state.focused || !this.props.renderInput)
{
document.body.removeEventListener('click', this.blurExt);
if (this.props.renderInput)
{
this.setState({ focused: false });
}
const f = this.props.onHide;
f && f();
}
}
@ -96,8 +97,8 @@ export default class Picker extends React.Component
render()
{
return (<React.Fragment>
{this.props.renderInput(this.getInputProps())}
{this.state.focused
{this.props.renderInput && this.props.renderInput(this.getInputProps())}
{!this.props.renderInput || this.state.focused
? <div style={{
position: 'fixed',
background: 'white',
@ -113,49 +114,85 @@ export default class Picker extends React.Component
</React.Fragment>);
}
componentDidMount()
{
this.componentDidUpdate();
}
componentDidUpdate()
{
if (this.state.focused && !this.state.height)
if (!this.props.renderInput || this.state.focused)
{
this.calculateDirection();
if (!this.state.height)
{
this.calculateDirection();
}
if (this.props.autoHide && !this._blurSet)
{
this._blurSet = true;
document.body.addEventListener('click', this.blurExt);
}
}
else if (this._blurSet)
{
this._blurSet = false;
document.body.removeEventListener('click', this.blurExt);
}
}
componentWillUnmount()
{
if (this._blurSet)
{
this._blurSet = false;
document.body.removeEventListener('click', this.blurExt);
}
}
calculateDirection()
{
if (!this.input || !this.picker)
if (!this.picker)
{
return;
}
const picker_size = ReactDOM.findDOMNode(this.picker).getBoundingClientRect();
const client = ReactDOM.findDOMNode(this.input).getBoundingClientRect();
const screen_width = window.innerWidth || document.documentElement.offsetWidth;
const screen_height = window.innerHeight || document.documentElement.offsetHeight;
let direction = this.props.direction;
if (!direction || direction === 'auto')
const inputRect = this.input
? ReactDOM.findDOMNode(this.input).getBoundingClientRect()
: { left: this.props.popupX||0, top: this.props.popupY||0 };
const pos = Picker.calculatePopupPosition(inputRect, this.picker, this.props);
if (this.state.top !== pos.top || this.state.left !== pos.left ||
this.state.width !== pos.width || this.state.height !== pos.height)
{
const down = client.top + picker_size.height < screen_height;
direction = down ? 'down' : 'up';
}
let top = client.top
+ (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop)
- (document.documentElement.clientTop || document.body.clientTop || 0);
const max_height = (direction == 'down' ? screen_height-top-client.height-32 : top-32);
const height = picker_size.height < max_height ? picker_size.height : max_height;
top = direction == 'down' ? (top + client.height) : (top - height);
let left = (client.left
+ (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft)
- (document.documentElement.clientLeft || document.body.clientLeft || 0));
if (left + picker_size.width > screen_width)
{
left = screen_width - picker_size.width;
}
let width = client.width > picker_size.width ? client.width : picker_size.width;
width = (this.props.minWidth && width < this.props.minWidth ? this.props.minWidth : width);
if (this.state.top !== top || this.state.left !== left ||
this.state.width !== width || this.state.height !== height)
{
this.setState({ top, left, width, height });
this.setState(pos);
}
}
static calculatePopupPosition(clientRect, popup, props)
{
const popup_size = ReactDOM.findDOMNode(popup).getBoundingClientRect();
const screen_width = window.innerWidth || document.documentElement.offsetWidth;
const screen_height = window.innerHeight || document.documentElement.offsetHeight;
let direction = props && props.direction;
if (!direction || direction === 'auto')
{
const down = clientRect.top + popup_size.height < screen_height ||
clientRect.top < screen_height/2;
direction = down ? 'down' : 'up';
}
let top = clientRect.top
+ (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop)
- (document.documentElement.clientTop || document.body.clientTop || 0);
const max_height = (direction == 'down' ? screen_height-top-(clientRect.height||0)-32 : top-32);
const height = Math.round(popup_size.height < max_height ? popup_size.height : max_height);
top = direction == 'down' ? (top + (clientRect.height||0)) : (top - height);
let left = (clientRect.left
+ (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft)
- (document.documentElement.clientLeft || document.body.clientLeft || 0));
if (left + popup_size.width > screen_width)
{
left = screen_width - popup_size.width;
}
let width = (clientRect.width||0) > popup_size.width ? clientRect.width : popup_size.width;
width = Math.round(props && props.minWidth && width < props.minWidth ? props.minWidth : width);
return { top, left, width, height };
}
}

View File

@ -21,6 +21,8 @@ export default class PickerMenu extends Picker
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]),
// change theme (CSS module) for this input
@ -50,8 +52,12 @@ export default class PickerMenu extends Picker
}
}
onMouseDown = () =>
onMouseDown = (ev) =>
{
if (ev)
{
this.props.keepOnClick ? ev.preventDefault() : this.blur();
}
const sel = this.props.items[this.state.active];
const f = this.props.onSelectItem;
f && f(sel);
@ -117,7 +123,7 @@ export default class PickerMenu extends Picker
componentDidUpdate()
{
super.componentDidUpdate();
if (this.input)
if (this.input && this.state.focused)
{
if (this.prevHeight && this.input.offsetHeight != this.prevHeight)
{
@ -126,11 +132,6 @@ export default class PickerMenu extends Picker
this.prevHeight = this.input.offsetHeight;
}
}
componentDidMount()
{
this.componentDidUpdate();
}
}
delete PickerMenu.propTypes.renderPicker;

View File

@ -24,6 +24,8 @@ export default class Selectbox extends React.PureComponent
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 id key - default "id"
@ -95,7 +97,11 @@ export default class Selectbox extends React.PureComponent
onSelectItem = (item) =>
{
this.setState({ query: null });
this.picker.blur();
if (this.props.keepFocusOnChange === false ||
this.props.keepFocusOnChange == null && !this.props.multiple)
{
this.picker.blur();
}
const sel = item[this.props.valueKey||'id'];
let value = sel;
if (this.props.multiple)
@ -265,7 +271,9 @@ export default class Selectbox extends React.PureComponent
return (<PickerMenu
ref={this.setPicker}
minWidth={this.props.minWidth}
clearOnClick={true}
autoHide={true}
keepOnClick={this.props.keepFocusOnChange === true ||
this.props.keepFocusOnChange == null && this.props.multiple}
renderInput={this.renderInput}
items={this.filtered_items}
labelKey={this.props.labelKey||'name'}

37
main.js
View File

@ -31,6 +31,17 @@ class Test extends React.PureComponent
this.setState({ value2: v });
}
showContextMenu = (ev) =>
{
this.setState({ ctx_x: ev.pageX, ctx_y: ev.pageY });
ev.preventDefault();
}
hideContextMenu = (ev) =>
{
this.setState({ ctx_x: null, ctx_y: null });
}
render()
{
return <div style={{padding: '20px', width: '300px', background: '#e0e8ff', fontSize: '13px'}}>
@ -38,6 +49,7 @@ class Test extends React.PureComponent
source={OPTIONS}
allowClear={true}
multiple={true}
keepFocusOnChange={true}
placeholder="Выберите значение"
suggestionMatch={true}
value={this.state.value}
@ -74,10 +86,31 @@ class Test extends React.PureComponent
onChange={this.onChange2}
/>
<PickerMenu
clearOnClick={true}
renderInput={p => <button {...p} className={button_css.button}>Меню</button>}
autoHide={true}
minWidth={200}
renderInput={p => <button
{...p}
focused={undefined}
style={{marginBottom: '20px'}}
className={button_css.button}>
Меню
</button>}
items={NAMES}
/>
<div style={{border: '1px solid #ccc', padding: '5px', background: 'white'}}
onContextMenu={this.showContextMenu}>
Кликните сюда правой кнопкой для вызова контекстного меню
</div>
{this.state.ctx_x != null
? <PickerMenu
autoHide={true}
minWidth={200}
items={NAMES}
popupX={this.state.ctx_x}
popupY={this.state.ctx_y}
onHide={this.hideContextMenu}
/>
: null}
</div>;
}
}