// "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 // License: LGPLv3.0+ // (c) Vitaliy Filippov 2019+ // Version 2020-04-27 import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; export default class Picker extends React.Component { static propTypes = { direction: PropTypes.string, autoHide: PropTypes.bool, minWidth: PropTypes.number, className: PropTypes.string, renderInput: PropTypes.func, renderPicker: PropTypes.func.isRequired, popupX: PropTypes.number, popupY: PropTypes.number, onHide: PropTypes.func, } state = { focused: false, height: 0, width: 0, top: 0, left: 0, } focus = () => { if (!this.state.focused && this.props.renderInput) { this.setState({ focused: true, height: 0 }); } } blur = () => { if (this.state.focused || !this.props.renderInput) { if (this.props.renderInput) { this.setState({ focused: false }); } const f = this.props.onHide; f && f(); } } 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; } getInputProps() { return { onFocus: this.focus, onBlur: this.blur, focused: this.state.focused, ref: this.setInput, }; } renderPicker() { return this.props.renderPicker(); } render() { return ( {this.props.renderInput && this.props.renderInput(this.getInputProps())} {!this.props.renderInput || this.state.focused ?
{this.renderPicker()}
: null}
); } componentDidMount() { this.componentDidUpdate(); } componentDidUpdate() { if (!this.props.renderInput || this.state.focused) { 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.picker) { return; } 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) { 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.ceil(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.ceil(props && props.minWidth && width < props.minWidth ? props.minWidth : width); return { top, left, width, height }; } }