Skip to content

Instantly share code, notes, and snippets.

@david1542
Created June 11, 2019 22:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save david1542/2d5f553a9483c342be033e85ed9e3642 to your computer and use it in GitHub Desktop.
Save david1542/2d5f553a9483c342be033e85ed9e3642 to your computer and use it in GitHub Desktop.
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