Add popup (no-input mode) support to Picker
parent
9544216475
commit
50d82f6fd4
127
Picker.js
127
Picker.js
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
12
Selectbox.js
12
Selectbox.js
|
@ -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
37
main.js
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue