UNPKG

kepler.gl

Version:

kepler.gl is a webgl based application to visualize large scale location data in the browser

307 lines (273 loc) 9.23 kB
// Copyright (c) 2018 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import React, {Component} from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import uniq from 'lodash.uniq'; import listensToClickOutside from 'react-onclickoutside'; import styled from 'styled-components'; import Accessor from './accessor'; import ChickletedInput from './chickleted-input'; import Typeahead from './typeahead'; import {Delete} from 'components/common/icons'; import DropdownList, {ListItem} from './dropdown-list'; /** * Converts non-arrays to arrays. Leaves arrays alone. Converts * undefined values to empty arrays ([] instead of [undefined]). * Otherwise, just returns [item] for non-array items. * * @param {*} item * @returns {array} boom! much array. very indexed. so useful. */ function _toArray(item) { if (Array.isArray(item)) { return item; } if (typeof item === 'undefined' || item === null) { return []; } return [item]; } const StyledDropdownSelect = styled.div` ${props => props.inputTheme === 'secondary' ? props.theme.secondaryInput : props.theme.input}; .list__item__anchor { ${props => props.theme.dropdownListAnchor}; } `; const DropdownSelectValue = styled.span` color: ${props => props.placeholder ? props.theme.selectColorPlaceHolder : props.theme.selectColor}; overflow: hidden; `; const DropdownSelectErase = styled.div` margin-left: 6px; display: flex; `; const DropdownWrapper = styled.div` background: ${props => props.theme.dropdownBgd}; border: 0; width: 100%; left: 0; z-index: 100; position: absolute; bottom: ${props => props.placement === 'top' ? props.theme.inputBoxHeight : 'auto'}; margin-top: ${props => (props.placement === 'bottom' ? '4px' : 'auto')}; margin-bottom: ${props => (props.placement === 'top' ? '4px' : 'auto')}; `; class ItemSelector extends Component { static propTypes = { // required properties selectedItems: PropTypes.oneOfType([ PropTypes.array, PropTypes.string, PropTypes.number, PropTypes.bool, PropTypes.object ]), onChange: PropTypes.func.isRequired, options: PropTypes.arrayOf(PropTypes.any).isRequired, // optional properties fixedOptions: PropTypes.arrayOf(PropTypes.any), erasable: PropTypes.bool, displayOption: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), getOptionValue: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), filterOption: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), placement: PropTypes.string, disabled: PropTypes.bool, isError: PropTypes.bool, multiSelect: PropTypes.bool, inputTheme: PropTypes.string, onBlur: PropTypes.func, placeholder: PropTypes.string, closeOnSelect: PropTypes.bool, DropdownHeaderComponent: PropTypes.func, DropDownRenderComponent: PropTypes.func, DropDownLineItemRenderComponent: PropTypes.func }; static defaultProps = { erasable: false, placement: 'bottom', selectedItems: [], displayOption: null, getOptionValue: null, filterOption: null, fixedOptions: null, inputTheme: 'primary', multiSelect: true, placeholder: 'Enter a value', closeOnSelect: true, searchable: true, dropdownHeader: null, DropdownHeaderComponent: null, DropDownRenderComponent: DropdownList, DropDownLineItemRenderComponent: ListItem }; state = { showTypeahead: false }; handleClickOutside = () => { this._hideTypeahead(); }; _hideTypeahead() { this.setState({showTypeahead: false}); this._onBlur(); } _onBlur = () => { // note: chickleted input is not a real form element so we call onBlur() // when we feel the events are appropriate if (this.props.onBlur) { this.props.onBlur(); } }; _removeItem = (item, e) => { // only used when multiSelect = true e.preventDefault(); e.stopPropagation(); const {selectedItems} = this.props; const index = selectedItems.findIndex(t => t === item); if (index < 0) { return; } const items = [ ...selectedItems.slice(0, index), ...selectedItems.slice(index + 1, selectedItems.length) ]; this.props.onChange(items); if (this.props.closeOnSelect) { this.setState({showTypeahead: false}); this._onBlur(); } }; _selectItem = item => { const getValue = Accessor.generateOptionToStringFor( this.props.getOptionValue || this.props.displayOption ); const previousSelected = _toArray(this.props.selectedItems); if (this.props.multiSelect) { const items = uniq(previousSelected.concat(_toArray(item).map(getValue))); this.props.onChange(items); } else { this.props.onChange(getValue(item)); } if (this.props.closeOnSelect) { this.setState({showTypeahead: false}); this._onBlur(); } }; _onErase = e => { e.stopPropagation(); this.props.onChange(null); }; _showTypeahead = () => { if (!this.props.disabled) { this.setState({ showTypeahead: true }); } }; _renderDropdown() { return ( <DropdownWrapper placement={this.props.placement}> <Typeahead customClasses={{ results: 'list-selector', input: 'typeahead__input', listItem: 'list__item', listAnchor: 'list__item__anchor' }} options={this.props.options} filterOption={this.props.filterOption} fixedOptions={this.props.fixedOptions} placeholder="Search" onOptionSelected={this._selectItem} customListComponent={this.props.DropDownRenderComponent} customListHeaderComponent={this.props.DropdownHeaderComponent} customListItemComponent={this.props.DropDownLineItemRenderComponent} displayOption={Accessor.generateOptionToStringFor( this.props.displayOption )} searchable={this.props.searchable} showOptionsWhenEmpty selectedItems={_toArray(this.props.selectedItems)} /> </DropdownWrapper> ); } render() { const selected = _toArray(this.props.selectedItems); const hasValue = selected.length; const displayOption = Accessor.generateOptionToStringFor( this.props.displayOption ); const dropdownSelectProps = { className: classnames(`item-selector__dropdown`, { active: this.state.showTypeahead }), disabled: this.props.disabled, onClick: this._showTypeahead, onFocus: this._showPopover, error: this.props.isError, inputTheme: this.props.inputTheme }; return ( <div className="item-selector"> <div style={{position: 'relative'}}> {/* this part is used to display the label */} {this.props.multiSelect ? ( <ChickletedInput {...dropdownSelectProps} selectedItems={_toArray(this.props.selectedItems)} placeholder={this.props.placeholder} displayOption={displayOption} removeItem={this._removeItem} /> ) : ( <StyledDropdownSelect {...dropdownSelectProps}> <DropdownSelectValue placeholder={!hasValue}> {hasValue ? ( <this.props.DropDownLineItemRenderComponent displayOption={displayOption} value={selected[0]} /> ) : ( this.props.placeholder )} </DropdownSelectValue> {this.props.erasable && hasValue ? ( <DropdownSelectErase> <Delete height="12px" onClick={this._onErase} /> </DropdownSelectErase> ) : null} </StyledDropdownSelect> )} {/* this part is used to built the list */} {this.state.showTypeahead && this._renderDropdown()} </div> </div> ); } }; export default listensToClickOutside(ItemSelector);