138 lines
4.1 KiB
JavaScript
138 lines
4.1 KiB
JavaScript
// "Generic dropdown component"
|
|
// Renders something and then when that "something" is focused renders a popup layer next to it
|
|
// For example, a text input with a popup selection list
|
|
// ...Or maybe a button with a popup menu
|
|
// (c) Vitaliy Filippov 2019+
|
|
// Version 2019-08-27
|
|
|
|
import React from 'react';
|
|
import ReactDOM from 'react-dom';
|
|
import PropTypes from 'prop-types';
|
|
|
|
export class Picker extends React.Component
|
|
{
|
|
static propTypes = {
|
|
direction: PropTypes.string,
|
|
clearOnClick: PropTypes.bool,
|
|
minWidth: PropTypes.number,
|
|
style: PropTypes.object,
|
|
renderInput: PropTypes.func.isRequired,
|
|
renderPicker: PropTypes.func.isRequired,
|
|
}
|
|
|
|
state = {
|
|
focused: false,
|
|
height: 0,
|
|
width: 0,
|
|
top: 0,
|
|
left: 0,
|
|
}
|
|
|
|
focus = () =>
|
|
{
|
|
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)
|
|
{
|
|
document.body.removeEventListener('click', this.blurExt);
|
|
}
|
|
}
|
|
|
|
blurExt = (ev) =>
|
|
{
|
|
let n = this.input ? ReactDOM.findDOMNode(this.input) : null;
|
|
let e = ev.target||ev.srcElement;
|
|
while (e)
|
|
{
|
|
// calendar-box is calendar.js's class
|
|
if (e == this.picker || e == n || /\bcalendar-box\b/.exec(e.className||''))
|
|
{
|
|
return;
|
|
}
|
|
e = e.parentNode;
|
|
}
|
|
this.blur();
|
|
}
|
|
|
|
setInput = (e) =>
|
|
{
|
|
this.input = e;
|
|
}
|
|
|
|
setPicker = (e) =>
|
|
{
|
|
this.picker = e;
|
|
}
|
|
|
|
render()
|
|
{
|
|
return (<div style={this.props.style}>
|
|
{this.props.renderInput({
|
|
onFocus: this.focus,
|
|
onBlur: this.blur,
|
|
focused: this.state.focused,
|
|
ref: this.setInput,
|
|
})}
|
|
{this.state.focused
|
|
? <div style={{
|
|
position: 'fixed',
|
|
background: 'white',
|
|
top: this.state.top,
|
|
width: this.state.width||'auto',
|
|
left: this.state.left,
|
|
zIndex: 100,
|
|
}} ref={this.setPicker}>
|
|
{this.props.renderPicker()}
|
|
</div>
|
|
: null}
|
|
</div>);
|
|
}
|
|
|
|
componentDidUpdate()
|
|
{
|
|
if (this.state.focused && !this.state.height)
|
|
{
|
|
this.calculateDirection();
|
|
}
|
|
}
|
|
|
|
calculateDirection()
|
|
{
|
|
if (!this.input || !this.picker)
|
|
{
|
|
return;
|
|
}
|
|
const picker_height = ReactDOM.findDOMNode(this.picker).getBoundingClientRect().height;
|
|
const client = ReactDOM.findDOMNode(this.input).getBoundingClientRect();
|
|
const screen_height = window.innerHeight || document.documentElement.offsetHeight;
|
|
let direction = this.props.direction;
|
|
if (!direction || direction === 'auto')
|
|
{
|
|
const down = client.top + picker_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);
|
|
top = direction == 'down' ? (top + client.height) + 'px' : (top - picker_height) + 'px';
|
|
const left = (client.left
|
|
+ (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft)
|
|
- (document.documentElement.clientLeft || document.body.clientLeft || 0)) + 'px';
|
|
const width = (this.props.minWidth && client.width < this.props.minWidth ? this.props.minWidth : client.width)+'px';
|
|
if (this.state.top !== top || this.state.left !== left ||
|
|
this.state.width !== width || this.state.height !== picker_height)
|
|
{
|
|
this.setState({ top, left, width, height: picker_height });
|
|
}
|
|
}
|
|
}
|