terriajs
Version:
Geospatial data visualization platform.
182 lines (156 loc) • 7.14 kB
JSX
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import defined from 'terriajs-cesium/Source/Core/defined';
import classNames from 'classnames';
import Styles from './dropdown.scss';
// Use this as drop down rather than the html <select> tag so we have more consistent styling
// Uses the contents of the element as the name of the dropdown if none selected.
const Dropdown = createReactClass({
propTypes: {
theme: PropTypes.object,
options: PropTypes.array, // Must be an array of objects with name properties. Uses <a> when there is an href property, else <button type='button'>.
selected: PropTypes.object,
selectOption: PropTypes.func, // The callback function; its arguments are the chosen object and its index.
textProperty: PropTypes.string, // property to display as text
matchWidth: PropTypes.bool,
children: PropTypes.any
},
getDefaultProps() {
return {
options: [],
selected: undefined,
textProperty: 'name',
align: 'left',
theme: {}
};
},
getInitialState() {
return {
isOpen: false
};
},
/* eslint-disable-next-line camelcase */
UNSAFE_componentWillMount() {
// this._element is updated by the ref callback attribute, https://facebook.github.io/react/docs/more-about-refs.html
this.buttonElement = undefined;
this.resetDownloadDropdownPosition();
},
resetDownloadDropdownPosition() {
setTimeout(() => {
if (!this.dismounted) {
this.setState({
dropdownPosition: {
top: 'inherit',
left: 'inherit',
right: 'inherit'
}
});
}
}, 100);
},
componentWillUnmount() {
this.removeListeners();
this.dismounted = true;
},
hideList() {
this.setState({
isOpen: false
});
this.resetDownloadDropdownPosition();
this.removeListeners();
},
removeListeners() {
document.body.removeEventListener('click', this.hideList);
this.buttonElement.removeEventListener('click', this.nativeButtonListener);
this.nativeButtonListener = undefined;
(this.scrollListeners || []).forEach(listenerElement => listenerElement.removeEventListener('scroll', this.hideList));
this.scrollListeners = undefined;
},
showList() {
// Add a listener to every ancestor capable of scrolling that will close the dropdown when this occurs.
const addScrollListeners = (element, listeningToSoFar) => {
if (element.scrollHeight > element.clientHeight) {
element.addEventListener('scroll', this.hideList);
listeningToSoFar.push(element);
}
if (element !== document.body) {
return addScrollListeners(element.parentNode, listeningToSoFar);
} else {
return listeningToSoFar;
}
};
this.scrollListeners = addScrollListeners(this.buttonElement, []);
// Figure out where the button is on the screen and set the dropdown's fixed position to right below it.
const outerDropdownPosition = this.buttonElement.getBoundingClientRect();
const dropdownPosition = {
top: outerDropdownPosition.bottom + 'px',
right: window.innerWidth - outerDropdownPosition.right + 'px'
};
if (this.props.matchWidth) {
dropdownPosition.left = outerDropdownPosition.left + 'px';
}
this.setState({
dropdownPosition,
isOpen: true
});
// Add the listener to be triggered when a click happens anywhere on the body (including the toggle button)
// or the outer panel is scrolled.
document.body.addEventListener('click', this.hideList);
// Unfortunately we need to add a native event listener because the native event hits document.body before
// the react event ever gets triggered.
this.nativeButtonListener = event => {
event.stopPropagation();
this.hideList();
};
this.buttonElement.addEventListener('click', this.nativeButtonListener);
},
select(option, index) {
this.props.selectOption(option, index);
this.hideList();
},
onButtonClicked() {
if (!this.state.isOpen) {
this.showList();
} else {
this.hideList();
}
},
render() {
const isOpenStyle = Styles.isOpen + ' ' + (this.props.theme.isOpen || '');
return (
<div className={classNames(Styles.dropdown, this.props.theme.dropdown)}>
<button type='button' onClick={this.onButtonClicked}
className={classNames(this.props.theme.button, Styles.btnDropdown)}
ref={element => {this.buttonElement = element;}}>
{defined(this.props.selected) ? this.props.selected[this.props.textProperty] : this.props.children}
{defined(this.props.theme.icon) ? this.props.theme.icon : null}
</button>
<ul className={classNames(Styles.list, this.props.theme.list, {[isOpenStyle]: this.state.isOpen})} style={this.state.dropdownPosition}>
<For each="option" of={this.props.options} index="index">
<li key={option[this.props.textProperty]}>
<Choose>
<When condition={option.href}>
<a href={option.href}
className={classNames(Styles.btnOption, this.props.theme.btnOption || '', {[Styles.isSelected]: option === this.props.selected})}
download={option.download}>
{option[this.props.textProperty]}
</a>
</When>
<Otherwise>
<button type='button'
className={classNames(Styles.btnOption, this.props.theme.btnOption || '', {[Styles.isSelected]: option === this.props.selected})}
onClick={() => this.select(option, index)}>
{option[this.props.textProperty]}
</button>
</Otherwise>
</Choose>
</li>
</For>
</ul>
</div>
);
}
});
module.exports = Dropdown;