terriajs
Version:
Geospatial data visualization platform.
182 lines (161 loc) • 5.72 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,
disabled: PropTypes.bool
},
getDefaultProps() {
return {
options: [],
selected: undefined,
textProperty: "name",
align: "left",
disabled: false,
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;
},
componentWillUnmount() {
this.removeListeners();
this.dismounted = true;
},
hideList() {
this.setState({
isOpen: false
});
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, []);
this.setState({
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;
}}
disabled={this.props.disabled}
>
{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
})}
>
<For each="option" of={this.props.options} index="index">
<li key={option[this.props.textProperty]}>
<Choose>
<When condition={option.href}>
<a
href={option.href}
target="_blank"
rel="noopener noreferrer"
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;