Created
June 11, 2019 22:31
-
-
Save david1542/2d5f553a9483c342be033e85ed9e3642 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react'; | |
import { connect } from 'react-redux'; | |
import { Button } from 'reactstrap'; | |
import styled from 'styled-components'; | |
import cloneDeep from 'lodash.clonedeep' | |
import { withBus } from 'react-bus'; | |
import { extractDeepValue } from '../../utils'; | |
import { | |
FETCH_COLLECTION | |
} from '../../actions'; | |
const OptionsContainer = styled.div` | |
position: absolute; | |
z-index: 3; | |
width: 280px; | |
top: 100%; | |
left: 0; | |
display: flex; | |
padding: 14px 10px 41px 10px; | |
flex-direction: column; | |
background-color: #fff; | |
background-clip: border-box; | |
border: 1px solid #c8ced3; | |
border-radius: 0.25rem; | |
`; | |
const ValuesText = styled.span` | |
margin-left: 14px; | |
font-size: 12px; | |
& > div > span { | |
font-weight: 600; | |
margin-left: 5px; | |
} | |
`; | |
const OptionsSearch = styled.input` | |
width: 100%; | |
height: 30px; | |
margin-bottom: 11px; | |
`; | |
const OptionsListStructure = styled.div` | |
display: flex; | |
flex-direction: column; | |
max-height: 310px; | |
overflow-y: auto; | |
&::-webkit-scrollbar-track { | |
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); | |
border-radius: 5px; | |
background-color: rgba(0,0,0,0.01); | |
} | |
&::-webkit-scrollbar { | |
width: 8px; | |
background-color: #F5F5F5; | |
} | |
&::-webkit-scrollbar-thumb { | |
border-radius: 5px; | |
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); | |
background-color: #555; | |
} | |
`; | |
const OptionsList = styled.div` | |
display: flex; | |
flex-direction: column; | |
`; | |
const OptionItem = styled.div` | |
width: 100%; | |
font-size: 14px; | |
color: black; | |
text-align: left; | |
transition: 0.3s; | |
cursor: pointer; | |
user-select: none; | |
&:hover { | |
background-color: rgba(0,0,0,0.03); | |
} | |
${(props) => props.checked ? ` | |
background-color: #cde7f1 !important; | |
` : null} | |
} | |
& > label { | |
padding: 11px 30px 11px 33px; | |
margin: 0; | |
font-size: 13px; | |
width: 100%; | |
display: block; | |
text-align: left; | |
color: #3C454C; | |
cursor: pointer; | |
position: relative; | |
z-index: 2; | |
transition: color 200ms ease-in; | |
overflow: hidden; | |
} | |
& > label:after { | |
width: 20px; | |
height: 20px; | |
left: 6px; | |
content: ''; | |
border: 2px solid #D1D7DC; | |
background-image: url("data:image/svg+xml,%3Csvg width='26' height='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.414 11L4 12.414l5.414 5.414L20.828 6.414 19.414 5l-10 10z' fill='%23fff' fill-rule='nonzero'/%3E%3C/svg%3E "); | |
background-repeat: no-repeat; | |
background-position: -2px -4px; | |
border-radius: 50%; | |
z-index: 2; | |
position: absolute; | |
right: 30px; | |
top: 50%; | |
-webkit-transform: translateY(-50%); | |
transform: translateY(-50%); | |
cursor: pointer; | |
transition: all 200ms ease-in; | |
} | |
& > input:checked ~ label:before { | |
-webkit-transform: translate(-50%, -50%) scale3d(56, 56, 1); | |
transform: translate(-50%, -50%) scale3d(56, 56, 1); | |
opacity: 1; | |
} | |
& > input:checked ~ label:after { | |
background-color: #506EEC; | |
border-color: #506EEC; | |
} | |
& > input { | |
width: 32px; | |
height: 32px; | |
order: 1; | |
z-index: 2; | |
position: absolute; | |
right: 30px; | |
top: 50%; | |
-webkit-transform: translateY(-50%); | |
transform: translateY(-50%); | |
cursor: pointer; | |
visibility: hidden; | |
} | |
`; | |
const NoItems = styled.div` | |
width: 100%; | |
font-size: 14px; | |
color: black; | |
text-align: left; | |
transition: 0.3s; | |
padding: 5px 7px; | |
`; | |
const SaveButton = styled.div` | |
width: 100%; | |
height: 31px; | |
box-sizing: border-box; | |
padding-bottom: 2px; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
background-color: #506EEC; | |
color: white; | |
font-size: 14px; | |
border-radius: 0.25rem; | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
border-top-left-radius: 0; | |
border-top-right-radius: 0; | |
cursor: pointer; | |
transition: opacity 0.3s; | |
&:hover { | |
opacity: 0.85; | |
} | |
`; | |
class AssociateField extends Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
open: false, | |
searchText: '' | |
}; | |
this.fieldContainer = React.createRef(); | |
this.searchInput = React.createRef(); | |
this.checkGlobalClick = this.checkGlobalClick.bind(this); | |
this.isOptionSelected = this.isOptionSelected.bind(this); | |
this.onOptionSelect = this.onOptionSelect.bind(this); | |
this.onSearchChange = this.onSearchChange.bind(this); | |
this.toggleOptions = this.toggleOptions.bind(this); | |
this.filterOptions = this.filterOptions.bind(this); | |
this.searchOptions = this.searchOptions.bind(this); | |
this.renderValues = this.renderValues.bind(this); | |
} | |
componentDidMount() { | |
const { options, listenToSearch, input } = this.props; | |
if (!options && !listenToSearch) { | |
this.searchOptions(); | |
} | |
if (listenToSearch && input) { | |
this.props.bus.on(`${input.name}-search`, this.searchOptions) | |
} | |
} | |
componentWillUnmount() { | |
const { input } = this.props; | |
if (input) { | |
this.props.bus.off(`${input.name}-search`, this.searchOptions) | |
} | |
} | |
searchOptions(query) { | |
const { fetchOptions, collection } = this.props; | |
fetchOptions(collection, query, true); | |
} | |
checkGlobalClick(e) { | |
if (e.target !== this.fieldContainer.current | |
&& e.target.parentNode !== this.fieldContainer.current | |
&& this.state.open) { | |
this.toggleOptions() | |
} | |
} | |
toggleOptions() { | |
this.setState((prevState) => ({ | |
open: !prevState.open, | |
searchText: '' | |
}), () => { | |
if (this.state.open && this.searchInput.current) { | |
this.searchInput.current.focus(); | |
} | |
}); | |
} | |
filterOptions() { | |
const { options, indicator } = this.props; | |
const { searchText } = this.state | |
// Search text filter | |
const result = options.filter(option => { | |
const indicatorValue = extractDeepValue(indicator, option); | |
return indicatorValue && indicatorValue.toLowerCase().includes(searchText.toLowerCase()); | |
}) | |
return result; | |
} | |
onSearchChange(e) { | |
this.setState({ | |
searchText: e.target.value | |
}); | |
} | |
onOptionSelect(id) { | |
const { input, multiple, onSelected } = this.props; | |
let isAdding = true | |
if (!multiple) { | |
if (input.value === id) { | |
input.onChange(false); | |
isAdding = false | |
} else { | |
input.onChange(id); | |
} | |
} else { | |
const fieldValue = input.value ? cloneDeep(input.value) : []; | |
const itemIndex = fieldValue.findIndex(item => item === id); | |
if (itemIndex === -1) { | |
fieldValue.push(id); | |
} else { | |
isAdding = false | |
fieldValue.splice(itemIndex, 1) | |
} | |
input.onChange(fieldValue); | |
} | |
if (onSelected && isAdding) { | |
const { options, valueProp } = this.props; | |
const valueProperty = valueProp || '_id'; | |
const selectedOption = options.find(option => { | |
return extractDeepValue(valueProperty, option) === id; | |
}); | |
onSelected(selectedOption) | |
} | |
} | |
isOptionSelected(option) { | |
const { valueProp, multiple, input } = this.props; | |
const valueProperty = valueProp || '_id'; | |
const optionValue = extractDeepValue(valueProperty, option); | |
if (!multiple) { | |
return input.value === optionValue | |
} else if (input.value) { | |
return input.value.some(item => item === optionValue) | |
} | |
return false; | |
} | |
renderOptions() { | |
const { open, searchText } = this.state; | |
if (open && this.props.options) { | |
const { indicator, valueProp } = this.props; | |
const options = this.filterOptions(); | |
const valueProperty = valueProp || '_id'; | |
const optionItems = options.map(option => { | |
const itemChecked = this.isOptionSelected(option); | |
const optionValue = extractDeepValue(valueProperty, option); | |
return ( | |
<OptionItem | |
key={optionValue} | |
> | |
<input | |
id={optionValue} | |
type="checkbox" | |
checked={itemChecked} | |
onChange={() => this.onOptionSelect(optionValue)} | |
/> | |
<label htmlFor={optionValue}> | |
{extractDeepValue(indicator, option)} | |
</label> | |
</OptionItem> | |
); | |
}) | |
const emptyView = <NoItems>No Items</NoItems> | |
const view = optionItems.length > 0 ? optionItems : emptyView | |
return ( | |
<OptionsContainer> | |
<OptionsSearch | |
className="form-control" | |
placeholder="Search" | |
value={searchText} | |
onChange={this.onSearchChange} | |
innerRef={this.searchInput} | |
> | |
</OptionsSearch> | |
<OptionsListStructure> | |
<OptionsList> | |
{view} | |
</OptionsList> | |
</OptionsListStructure> | |
<SaveButton onClick={this.toggleOptions}>Save</SaveButton> | |
</OptionsContainer> | |
); | |
} | |
return null; | |
} | |
renderValues() { | |
const { options, indicator, multiple } = this.props; | |
if (options) { | |
const selectedValues = options.filter(option => { | |
return this.isOptionSelected(option); | |
}).map(option => extractDeepValue(indicator, option)) | |
.join(', '); | |
const label = multiple ? 'Current associations:' : 'Current association:' | |
const valuesLabel = ( | |
<div> | |
{label} | |
<span>{selectedValues}</span> | |
</div> | |
) | |
return ( | |
<ValuesText> | |
{selectedValues.length > 0 ? | |
valuesLabel | |
: | |
'No values associated' | |
} | |
</ValuesText> | |
) | |
} | |
return null; | |
} | |
render() { | |
const containerStyle = { | |
display: 'flex', | |
position: 'relative', | |
alignItems: 'center' | |
}; | |
return ( | |
<div | |
onClick={(e) => e.stopPropagation()} | |
ref={this.fieldContainer} | |
style={containerStyle} | |
> | |
<Button | |
color="primary" | |
type="button" | |
style={{ position: 'relative' }} | |
onClick={this.toggleOptions} | |
> | |
<i | |
className="fa fa-link" | |
aria-hidden="true" | |
style={{ | |
marginLeft: '-2px', | |
marginRight: '7px' | |
}} | |
></i> | |
Associate | |
</Button> | |
{this.renderValues()} | |
{this.renderOptions()} | |
</div> | |
); | |
} | |
} | |
const mapStateToProps = (state, ownProps) => ({ | |
options: state.reducer[ownProps.collection] | |
}); | |
const mapDispatchToProps = (dispatch) => ({ | |
fetchOptions: (collection, params, hideLoading) => dispatch({ | |
type: FETCH_COLLECTION, | |
payload: collection, | |
options: { | |
params, | |
hideLoading | |
} | |
}) | |
}) | |
export default connect( | |
mapStateToProps, | |
mapDispatchToProps | |
)(withBus()(AssociateField)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment