UNPKG

react-cm-ui

Version:
632 lines (577 loc) 20 kB
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}>&nbsp;*</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);