react-cm-ui
Version:
React UI for Healthy Church
632 lines (577 loc) • 20 kB
JSX
import {
find,
get,
isEmpty,
isFunction,
isString,
map,
range,
} from 'lodash';
import ClassNames from 'classnames';
import moment from 'moment-timezone';
import MomentPropTypes from 'react-moment-proptypes';
import PropTypes from 'prop-types';
import React from 'react';
import {
BEM_BLOCK_NAME,
DAY_SINGULAR,
DAYS_MAX,
DAYS_PLURAL,
HOUR_SINGULAR,
HOURS_MAX,
HOURS_PLURAL,
MINUTE_SINGULAR,
MINUTES_MAX,
MINUTES_PLURAL,
MONTH_SINGULAR,
MONTHS_MAX,
MONTHS_PLURAL,
SECOND_SINGULAR,
SECONDS_MAX,
SECONDS_PLURAL,
YEAR_SINGULAR,
YEARS_PLURAL,
} from './constants';
import { UI_CLASS_NAME } from '../../global/constants';
import { withStyles } from '../../styles';
import Dropdown from '../dropdown';
import Grid from '../../layout/grid';
import Typography from '../../dataDisplay/typography';
const propTypes = {
/**
* Override or extend the styles applied to the DurationPicker.
*/
classes: PropTypes.shape({
root: PropTypes.string,
dropdown: PropTypes.string,
dropdownColumn: PropTypes.string,
error: PropTypes.string,
errorColumn: PropTypes.string,
grid: PropTypes.string,
label: PropTypes.string,
labelColumn: PropTypes.string,
required: PropTypes.string,
}),
/**
* Assign additional class names to the DurationPicker.
*/
className: PropTypes.string,
/**
* Deprecated prop. Please use `disabled` instead.
*/
disable: PropTypes.bool,
/**
* A DurationPicker can be disabled.
*/
disabled: PropTypes.bool,
/**
* Indicate that there is a validation error for this DurationPicker input control.
*/
error: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.string,
]),
/**
* Specify an element ID this DurationPicker input control.
*/
id: PropTypes.string,
/**
* Specify a label for the DurationPicker input control.
*/
label: PropTypes.string,
/**
* If there is a years component, specify the maximum value to which it can be set.
*/
maxYears: PropTypes.number,
/**
* Event handler for consumer to control state outside of the DurationPicker.
*/
onChange: PropTypes.func,
/**
* Indicate whether the DurationPicker input control represents a required field.
*/
required: PropTypes.bool,
/**
* Indicate whether the DurationPicker includes a Days component.
*/
showDays: PropTypes.bool,
/**
* Indicate whether the DurationPicker includes an Hours component.
*/
showHours: PropTypes.bool,
/**
* Indicate whether the DurationPicker includes a Minutes component.
*/
showMinutes: PropTypes.bool,
/**
* Indicate whether the DurationPicker includes a Months component.
*/
showMonths: PropTypes.bool,
/**
* Indicate whether the DurationPicker includes a Seconds component.
*/
showSeconds: PropTypes.bool,
/**
* Indicate whether the DurationPicker includes a Years component.
*/
showYears: PropTypes.bool,
/**
* The value of the DurationPicker. Should be a `Duration` object from the Moment JS library.
*/
value: MomentPropTypes.momentDurationObj,
};
const defaultProps = {
classes: null,
className: null,
disable: false,
disabled: false,
error: null,
id: null,
label: null,
maxYears: 99,
onChange: null,
required: false,
showDays: true,
showHours: true,
showMinutes: false,
showMonths: false,
showSeconds: false,
showYears: false,
value: null,
};
const defaultState = {
years: 0,
months: 0,
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
value: null,
};
const useStyles = (theme) => ({
root: {
},
grid: {
'&.ui.grid': {
margin: -5.5,
},
},
dropdown: {
'&.ui.dropdown.dropdown-selection': {
minWidth: 'auto',
},
},
dropdownColumn: {
flexBasis: 106,
flexGrow: 1,
maxWidth: 106, // 95px content + 11px padding
minWidth: 'auto',
padding: 5.5,
},
error: {
color: theme.palette.error.main,
'& .ui.dropdown .Select-control': {
borderColor: theme.palette.error.main,
},
},
errorColumn: {
padding: 5.5,
},
label: {
color: theme.palette.text.secondary,
},
labelColumn: {
padding: 5.5,
},
required: {
color: theme.palette.error.main,
},
});
/**
* The DurationPicker uses a series of Drodown select menus for different units
* of time (seconds, minutes, hours, days, months and years) to allow the user
* to configure and input a single duration or timespan value (stored as a
* Duration object from the MomentJS library).
*/
class DurationPicker extends React.PureComponent {
static areMomentDurationsEqual(durationA, durationB) {
const durationAHasValue = moment.isDuration(durationA);
const durationBHasValue = moment.isDuration(durationB);
if (!durationAHasValue && !durationBHasValue) {
return true;
}
if (!durationAHasValue || !durationBHasValue) {
return false;
}
// both are valid moment duration objects
return durationA.valueOf() === durationB.valueOf();
}
static buildMomentDuration(updatedState) {
const {
days,
hours,
minutes,
months,
seconds,
years,
} = updatedState;
return moment.duration({
seconds,
minutes,
hours,
days,
months,
years,
});
}
static getStateFromDurationValue(duration) {
if (!moment.isDuration(duration)) {
return null;
}
return {
days: duration.days(),
hours: duration.hours(),
minutes: duration.minutes(),
months: duration.months(),
seconds: duration.seconds(),
years: duration.years(),
value: duration,
};
}
static haveUnitOfTimeOptionsChanged(props, prevProps) {
return props.showDays !== prevProps.showDays ||
props.showHours !== prevProps.showHours ||
props.showMinutes !== prevProps.showMinutes ||
props.showMonths !== prevProps.showMonths ||
props.showSeconds !== prevProps.showSeconds ||
props.showYears !== prevProps.showYears;
}
static isValidValue(value) {
return moment.isDuration(value) &&
value.valueOf() !== moment.duration().valueOf();
}
constructor(props) {
super(props);
const state = DurationPicker.getStateFromDurationValue(props.value) || defaultState;
this.state = {
...state,
};
this.onStateChange = this.onStateChange.bind(this);
this.onDaysChange = this.onDaysChange.bind(this);
this.onHoursChange = this.onHoursChange.bind(this);
this.onMinutesChange = this.onMinutesChange.bind(this);
this.onMonthsChange = this.onMonthsChange.bind(this);
this.onSecondsChange = this.onSecondsChange.bind(this);
this.onYearsChange = this.onYearsChange.bind(this);
this.setupDropdownOptions();
}
componentDidUpdate(prevProps, prevState) {
const {
value: prevValue,
} = prevState;
const {
value,
} = this.props;
// Handle external value change ... sync up internal state
if (!DurationPicker.areMomentDurationsEqual(prevValue, value)) {
if (moment.isDuration(value)) {
this.setState(DurationPicker.getStateFromDurationValue(value));
} else { // if value is nil or invalid, reset to default state
this.setState(defaultState);
}
}
// If any `showXXX` props have changed, ensure the dropdown options are populated
if (DurationPicker.haveUnitOfTimeOptionsChanged(this.props, prevProps)) {
this.setupDropdownOptions();
}
}
onDaysChange(selectedOption) {
const value = get(selectedOption, 'value', 0);
this.onStateChange({ days: value });
}
onHoursChange(selectedOption) {
const value = get(selectedOption, 'value', 0);
this.onStateChange({ hours: value });
}
onMinutesChange(selectedOption) {
const value = get(selectedOption, 'value', 0);
this.onStateChange({ minutes: value });
}
onMonthsChange(selectedOption) {
const value = get(selectedOption, 'value', 0);
this.onStateChange({ months: value });
}
onSecondsChange(selectedOption) {
const value = get(selectedOption, 'value', 0);
this.onStateChange({ seconds: value });
}
onYearsChange(selectedOption) {
const value = get(selectedOption, 'value', 0);
this.onStateChange({ years: value });
}
onStateChange(updatedState) {
const updatedMomentDurationValue = DurationPicker.buildMomentDuration({
...this.state,
...updatedState,
});
const { onChange } = this.props;
if (isFunction(onChange)) { // controlled component; we own the unit of time component states, but _not_ the complete value state
this.setState(updatedState); // update the local state that we do own
onChange(updatedMomentDurationValue); // call `onChange()` with the updated value
} else { // uncontrolled component ... we own the complete value state
const finalUpdatedState = {
...updatedState,
value: updatedMomentDurationValue,
};
this.setState(finalUpdatedState);
}
}
setupDropdownOptions() {
const {
maxYears,
showDays,
showHours,
showMinutes,
showMonths,
showSeconds,
showYears,
} = this.props;
this.secondsOptions = (showSeconds &&
isEmpty(this.secondsOptions) &&
map(
range(SECONDS_MAX + 1),
(seconds) => ({
label: `${seconds} ${seconds === 1 ? SECOND_SINGULAR : SECONDS_PLURAL}`,
value: seconds,
}),
)) || this.secondsOptions || [];
this.minutesOptions = (showMinutes &&
isEmpty(this.minutesOptions) &&
map(
range(MINUTES_MAX + 1),
(minutes) => ({
label: `${minutes} ${minutes === 1 ? MINUTE_SINGULAR : MINUTES_PLURAL}`,
value: minutes,
}),
)) || this.minutesOptions || [];
this.hoursOptions = (showHours &&
isEmpty(this.hoursOptions) &&
map(
range(HOURS_MAX + 1),
(hours) => ({
label: `${hours} ${hours === 1 ? HOUR_SINGULAR : HOURS_PLURAL}`,
value: hours,
}),
)) || this.hoursOptions || [];
this.daysOptions = (showDays &&
isEmpty(this.daysOptions) &&
map(
range(DAYS_MAX + 1),
(days) => ({
label: `${days} ${days === 1 ? DAY_SINGULAR : DAYS_PLURAL}`,
value: days,
}),
)) || this.daysOptions || [];
this.monthsOptions = (showMonths &&
isEmpty(this.monthsOptions) &&
map(
range(MONTHS_MAX + 1),
(months) => ({
label: `${months} ${months === 1 ? MONTH_SINGULAR : MONTHS_PLURAL}`,
value: months,
}),
)) || this.monthsOptions || [];
this.yearsOptions = (showYears &&
isEmpty(this.yearsOptions) &&
map(
range(maxYears + 1),
(years) => ({
label: `${years} ${years === 1 ? YEAR_SINGULAR : YEARS_PLURAL}`,
value: years,
}),
)) || this.yearsOptions || [];
}
render() {
const {
classes,
className,
disable,
disabled,
error,
id,
label,
required,
showDays,
showHours,
showMinutes,
showMonths,
showSeconds,
showYears,
} = this.props;
const {
days,
hours,
minutes,
months,
seconds,
years,
value,
} = this.state;
const rootClasses = ClassNames(
classes.root,
UI_CLASS_NAME,
BEM_BLOCK_NAME,
className,
{
[classes.error]: !!error,
},
);
const realId = id || BEM_BLOCK_NAME;
const isDisabled = disabled || disable;
return (
<div
className={rootClasses}
id={realId}
>
<Grid className={classes.grid}>
{label && (
<Grid.Column
className={classes.labelColumn}
width={12}
>
<Typography
classes={{
body2: classes.label,
}}
component="label"
variant="body2"
>
{label}
{required && !DurationPicker.isValidValue(value) ? (
<span className={classes.required}> *</span>
) : null}
</Typography>
</Grid.Column>
)}
{showYears && (
<Grid.Column
className={classes.dropdownColumn}
>
<Dropdown
className={classes.dropdown}
clearable={false}
disable={isDisabled}
fluid
id={`${id || BEM_BLOCK_NAME}--years_dropdown`}
onChange={this.onYearsChange}
options={this.yearsOptions}
selection
value={find(this.yearsOptions, (opt) => opt.value === years)}
/>
</Grid.Column>
)}
{showMonths && (
<Grid.Column
className={classes.dropdownColumn}
>
<Dropdown
className={classes.dropdown}
clearable={false}
disable={isDisabled}
fluid
id={`${id || BEM_BLOCK_NAME}--months_dropdown`}
onChange={this.onMonthsChange}
options={this.monthsOptions}
selection
value={find(this.monthsOptions, (opt) => opt.value === months)}
/>
</Grid.Column>
)}
{showDays && (
<Grid.Column
className={classes.dropdownColumn}
>
<Dropdown
className={classes.dropdown}
clearable={false}
disable={isDisabled}
fluid
id={`${id || BEM_BLOCK_NAME}--days_dropdown`}
onChange={this.onDaysChange}
options={this.daysOptions}
selection
value={find(this.daysOptions, (opt) => opt.value === days)}
/>
</Grid.Column>
)}
{showHours && (
<Grid.Column
className={classes.dropdownColumn}
>
<Dropdown
className={classes.dropdown}
clearable={false}
disable={isDisabled}
fluid
id={`${id || BEM_BLOCK_NAME}--hours_dropdown`}
onChange={this.onHoursChange}
options={this.hoursOptions}
selection
value={find(this.hoursOptions, (opt) => opt.value === hours)}
/>
</Grid.Column>
)}
{showMinutes && (
<Grid.Column
className={classes.dropdownColumn}
>
<Dropdown
className={classes.dropdown}
clearable={false}
disable={isDisabled}
fluid
id={`${id || BEM_BLOCK_NAME}--minutes_dropdown`}
onChange={this.onMinutesChange}
options={this.minutesOptions}
selection
value={find(this.minutesOptions, (opt) => opt.value === minutes)}
/>
</Grid.Column>
)}
{showSeconds && (
<Grid.Column
className={classes.dropdownColumn}
>
<Dropdown
className={classes.dropdown}
clearable={false}
disable={isDisabled}
fluid
id={`${id || BEM_BLOCK_NAME}--seconds_dropdown`}
onChange={this.onSecondsChange}
options={this.secondsOptions}
selection
value={find(this.secondsOptions, (opt) => opt.value === seconds)}
/>
</Grid.Column>
)}
{isString(error) && (
<Grid.Column className={classes.errorColumn} width={12}>
<Typography
classes={{
body2: classes.error,
}}
variant="body2"
>
{error}
</Typography>
</Grid.Column>
)}
</Grid>
</div>
);
}
}
DurationPicker.propTypes = propTypes;
DurationPicker.defaultProps = defaultProps;
export default withStyles(useStyles, { withTheme: true })(DurationPicker);