diff --git a/lib/src/DatePicker/Calendar.jsx b/lib/src/DatePicker/Calendar.jsx index 04d16f4a0..ed1f77986 100644 --- a/lib/src/DatePicker/Calendar.jsx +++ b/lib/src/DatePicker/Calendar.jsx @@ -16,7 +16,7 @@ import withUtils from '../_shared/WithUtils'; /* eslint-disable no-unused-expressions */ export class Calendar extends Component { static propTypes = { - date: PropTypes.object.isRequired, + date: DomainPropTypes.dateRange.isRequired, minDate: DomainPropTypes.date, maxDate: DomainPropTypes.date, classes: PropTypes.object.isRequired, @@ -30,6 +30,8 @@ export class Calendar extends Component { shouldDisableDate: PropTypes.func, utils: PropTypes.object.isRequired, allowKeyboardControl: PropTypes.bool, + multi: PropTypes.bool, + range: PropTypes.bool, }; static defaultProps = { @@ -42,18 +44,22 @@ export class Calendar extends Component { renderDay: undefined, allowKeyboardControl: false, shouldDisableDate: () => false, + multi: false, + range: false, }; state = { slideDirection: 'left', - currentMonth: this.props.utils.getStartOfMonth(this.props.date), + currentMonth: this.props.utils.getStartOfMonth( + this.props.date.length > 0 ? this.props.date[this.props.date.length - 1] : this.props.utils.date()), }; static getDerivedStateFromProps(nextProps, state) { if (!nextProps.utils.isEqual(nextProps.date, state.lastDate)) { return { lastDate: nextProps.date, - currentMonth: nextProps.utils.getStartOfMonth(nextProps.date), + currentMonth: nextProps.utils.getStartOfMonth( + nextProps.date.length > 0 ? nextProps.date[nextProps.date.length - 1] : nextProps.utils.date()), }; } @@ -65,22 +71,45 @@ export class Calendar extends Component { date, minDate, maxDate, utils, disableFuture, disablePast, } = this.props; - if (this.shouldDisableDate(date)) { + date.forEach(day => + this.shouldDisableDate(day) && this.onDateSelect(findClosestEnabledDate({ - date, + day, utils, minDate, maxDate, disablePast, disableFuture, shouldDisableDate: this.shouldDisableDate, - }), false); - } + }), false) + ); } onDateSelect = (day, isFinish = true) => { - const { date, utils } = this.props; - this.props.onChange(utils.mergeDateAndTime(day, date), isFinish); + const { date, utils, multi, range } = this.props; + + let newDate = day + if (date.length > 0) { + newDate = utils.mergeDateAndTime(day, date[0]); + } + + if (multi) { + let i = date.findIndex(o => utils.isEqual(o, day)) + if (i === -1) { + newDate = date.concat(newDate) + } else { + newDate = [ ...date ] + newDate.splice(i, 1) + } + } else if (range && date.length === 1) { + newDate = utils.isAfter(newDate, date[0]) + ? [ date[0], newDate ] + : [ newDate, date[0] ]; + } else { + newDate = [ newDate ] + } + + this.props.onChange(newDate, isFinish); }; handleChangeMonth = (newMonth, slideDirection) => { @@ -184,9 +213,10 @@ export class Calendar extends Component { } renderDays = (week) => { - const { date, renderDay, utils } = this.props; + const { date, renderDay, utils, range } = this.props; + const { hover } = this.state; - const selectedDate = utils.startOfDay(date); + const selectedDate = date.map(utils.startOfDay); const currentMonthNumber = utils.getMonth(this.state.currentMonth); const now = utils.date(); @@ -194,12 +224,24 @@ export class Calendar extends Component { const disabled = this.shouldDisableDate(day); const dayInCurrentMonth = utils.getMonth(day) === currentMonthNumber; + let additionalProps; + + if (range) { + additionalProps = { + prelighted: date.length === 1 && utils.isBetween(day, date[0], hover), + highlighted: date.length > 1 && utils.isBetween(day, date[0], date[1]), + leftCap: utils.isEqual(day, Math.min(date[0], date[1] || hover)), + rightCap: utils.isEqual(day, Math.max(date[0], date[1] || hover)), + } + } + let dayComponent = ( @@ -216,6 +258,7 @@ export class Calendar extends Component { dayInCurrentMonth={dayInCurrentMonth} disabled={disabled} onSelect={this.onDateSelect} + onMouseEnter={e => this.setState({ hover: day })} > {dayComponent} diff --git a/lib/src/DatePicker/DatePicker.jsx b/lib/src/DatePicker/DatePicker.jsx index 8cc617203..5569000dc 100644 --- a/lib/src/DatePicker/DatePicker.jsx +++ b/lib/src/DatePicker/DatePicker.jsx @@ -10,7 +10,7 @@ import withUtils from '../_shared/WithUtils'; export class DatePicker extends PureComponent { static propTypes = { - date: PropTypes.object.isRequired, + date: DomainPropTypes.dateRange.isRequired, minDate: DomainPropTypes.date, maxDate: DomainPropTypes.date, onChange: PropTypes.func.isRequired, @@ -25,6 +25,8 @@ export class DatePicker extends PureComponent { utils: PropTypes.object.isRequired, shouldDisableDate: PropTypes.func, allowKeyboardControl: PropTypes.bool, + multi: PropTypes.bool, + range: PropTypes.bool, } static defaultProps = { @@ -40,6 +42,8 @@ export class DatePicker extends PureComponent { rightArrowIcon: undefined, renderDay: undefined, shouldDisableDate: undefined, + multi: false, + range: false, } state = { @@ -47,7 +51,7 @@ export class DatePicker extends PureComponent { } get date() { - return this.props.utils.startOfDay(this.props.date); + return this.props.date.map(this.props.utils.startOfDay); } get minDate() { @@ -83,6 +87,8 @@ export class DatePicker extends PureComponent { utils, shouldDisableDate, allowKeyboardControl, + multi, + range, } = this.props; const { showYearSelection } = this.state; @@ -93,14 +99,14 @@ export class DatePicker extends PureComponent { variant="subheading" onClick={this.openYearSelection} selected={showYearSelection} - label={utils.getYearText(this.date)} + label={utils.getYearText(this.date[this.date.length - 1])} /> @@ -133,6 +139,8 @@ export class DatePicker extends PureComponent { utils={utils} shouldDisableDate={shouldDisableDate} allowKeyboardControl={allowKeyboardControl} + multi={multi} + range={range} /> } diff --git a/lib/src/DatePicker/DatePickerWrapper.jsx b/lib/src/DatePicker/DatePickerWrapper.jsx index e5c19df75..8f867db56 100644 --- a/lib/src/DatePicker/DatePickerWrapper.jsx +++ b/lib/src/DatePicker/DatePickerWrapper.jsx @@ -26,6 +26,9 @@ export const DatePickerWrapper = (props) => { rightArrowIcon, shouldDisableDate, value, + multi, + range, + formatSeperator, ...other } = props; @@ -57,6 +60,7 @@ export const DatePickerWrapper = (props) => { ref={forwardedRef} value={value} isAccepted={isAccepted} + formatSeperator={formatSeperator} {...other} > { renderDay={renderDay} rightArrowIcon={rightArrowIcon} shouldDisableDate={shouldDisableDate} + multi={multi} + range={range} /> ) @@ -83,7 +89,7 @@ export const DatePickerWrapper = (props) => { DatePickerWrapper.propTypes = { /** Datepicker value */ - value: DomainPropTypes.date, + value: DomainPropTypes.dateRange, /** Min selectable date */ minDate: DomainPropTypes.date, /** Max selectable date */ @@ -117,10 +123,16 @@ DatePickerWrapper.propTypes = { /** Enables keyboard listener for moving between days in calendar */ allowKeyboardControl: PropTypes.bool, forwardedRef: PropTypes.func, + /** Enables selecting multiple dates **/ + multi: PropTypes.bool, + /** Enables selecting a range of dates **/ + range: PropTypes.bool, + /** String to join multiple values **/ + formatSeperator: PropTypes.string, }; DatePickerWrapper.defaultProps = { - value: new Date(), + value: [ new Date() ], format: 'MMMM Do', autoOk: false, minDate: '1900-01-01', @@ -137,6 +149,9 @@ DatePickerWrapper.defaultProps = { labelFunc: undefined, shouldDisableDate: undefined, forwardedRef: undefined, + multi: false, + range: false, + formatSeperator: ', ', }; export default React.forwardRef((props, ref) => ( diff --git a/lib/src/DatePicker/Day.jsx b/lib/src/DatePicker/Day.jsx index 0c3f033d6..eb536e4db 100644 --- a/lib/src/DatePicker/Day.jsx +++ b/lib/src/DatePicker/Day.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import withStyles from '@material-ui/core/styles/withStyles'; import IconButton from '@material-ui/core/IconButton'; +import { fade } from '@material-ui/core/styles/colorManipulator'; class Day extends PureComponent { static propTypes = { @@ -23,17 +24,18 @@ class Day extends PureComponent { render() { const { - children, classes, disabled, hidden, current, selected, ...other + children, classes, disabled, hidden, current, selected, + prelighted, highlighted, leftCap, rightCap, ...other } = this.props; const className = classnames(classes.day, { [classes.hidden]: hidden, [classes.current]: current, - [classes.selected]: selected, + [classes.selected]: selected || highlighted, [classes.disabled]: disabled, }); - return ( + const icon = ( {children} ); + + if (highlighted || prelighted) { + return ( +
+ {icon} +
+ ); + } else { + return icon; + } } } @@ -74,6 +91,20 @@ const styles = theme => ({ pointerEvents: 'none', color: theme.palette.text.hint, }, + prelighted: { + backgroundColor: fade(theme.palette.action.active, theme.palette.action.hoverOpacity), + }, + highlighted: { + backgroundColor: theme.palette.primary.main, + }, + leftCap: { + borderTopLeftRadius: '50%', + borderBottomLeftRadius: '50%', + }, + rightCap: { + borderTopRightRadius: '50%', + borderBottomRightRadius: '50%', + }, }); export default withStyles(styles, { name: 'MuiPickersDay' })(Day); diff --git a/lib/src/DatePicker/YearSelection.jsx b/lib/src/DatePicker/YearSelection.jsx index 5b878a2f6..6a1b1e035 100644 --- a/lib/src/DatePicker/YearSelection.jsx +++ b/lib/src/DatePicker/YearSelection.jsx @@ -8,7 +8,7 @@ import Year from './Year'; export class YearSelection extends PureComponent { static propTypes = { - date: PropTypes.shape({}).isRequired, + date: DomainPropTypes.dateRange.isRequired, minDate: DomainPropTypes.date.isRequired, maxDate: DomainPropTypes.date.isRequired, classes: PropTypes.object.isRequired, @@ -30,7 +30,7 @@ export class YearSelection extends PureComponent { onYearSelect = (year) => { const { date, onChange, utils } = this.props; - const newDate = utils.setYear(date, year); + const newDate = date.map(o => utils.setYear(o, year)); onChange(newDate); } @@ -55,7 +55,7 @@ export class YearSelection extends PureComponent { const { minDate, maxDate, date, classes, disablePast, disableFuture, utils, } = this.props; - const currentYear = utils.getYear(date); + const currentYear = utils.getYear(date[date.length - 1]); return (
diff --git a/lib/src/DateTimePicker/DateTimePicker.jsx b/lib/src/DateTimePicker/DateTimePicker.jsx index adf7622ea..6c479ddeb 100644 --- a/lib/src/DateTimePicker/DateTimePicker.jsx +++ b/lib/src/DateTimePicker/DateTimePicker.jsx @@ -21,7 +21,7 @@ export class DateTimePicker extends Component { animateYearScrolling: PropTypes.bool, autoSubmit: PropTypes.bool, classes: PropTypes.object.isRequired, - date: PropTypes.object.isRequired, + date: DomainPropTypes.dateRange.isRequired, dateRangeIcon: PropTypes.node, disableFuture: PropTypes.bool, disablePast: PropTypes.bool, @@ -125,7 +125,7 @@ export class DateTimePicker extends Component { return ( { {...other} > { - const initialDate = value || initialFocusedDate || utils.date(); - const date = utils.date(initialDate); + const date = utils.ensureArray(value || initialFocusedDate).map(utils.date); - return utils.isValid(date) ? date : utils.date(); + if (date.every(utils.isValid) && date.length !== 0) + return date; + else if (initialFocusedDate === false) + return []; + else + return [ utils.date() ]; }; export const BasePickerHoc = compose( @@ -21,7 +25,7 @@ export const BasePickerHoc = compose( withState('isAccepted', 'handleAcceptedChange', false), lifecycle({ componentDidUpdate(prevProps) { - if (prevProps.value !== this.props.value) { + if (!this.props.utils.isEqual(prevProps.value, this.props.value)) { this.props.changeDate(getInitialDate(this.props)); } }, @@ -29,7 +33,7 @@ export const BasePickerHoc = compose( withHandlers({ handleClear: ({ onChange }) => () => onChange(null), handleAccept: ({ onChange, date }) => () => onChange(date), - handleSetTodayDate: ({ changeDate, utils }) => () => changeDate(utils.date()), + handleSetTodayDate: ({ changeDate, utils }) => () => changeDate([ utils.date() ]), handleTextFieldChange: ({ changeDate, onChange }) => (date) => { if (date === null) { onChange(null); @@ -62,4 +66,3 @@ export const BasePickerHoc = compose( ); export default withRenderProps(BasePickerHoc); - diff --git a/lib/src/_shared/DateTextField.jsx b/lib/src/_shared/DateTextField.jsx index 4e19ba575..b162f3f67 100644 --- a/lib/src/_shared/DateTextField.jsx +++ b/lib/src/_shared/DateTextField.jsx @@ -12,22 +12,24 @@ import withUtils from '../_shared/WithUtils'; const getDisplayDate = (props) => { const { - utils, value, format, invalidLabel, emptyLabel, labelFunc, + utils, format, invalidLabel, emptyLabel, labelFunc, formatSeperator } = props; + const value = utils.ensureArray(props.value); - const isEmpty = value === null; - const date = utils.date(value); + const isEmpty = value == null || value.length < 1 || (value.length == 1 && value[0] == null); - if (labelFunc) { - return labelFunc(isEmpty ? null : date, invalidLabel); - } + const date = value.map(utils.date); if (isEmpty) { return emptyLabel; } - return utils.isValid(date) - ? utils.format(date, format) + if (labelFunc) { + return labelFunc(date, invalidLabel); + } + + return date.every(utils.isValid) + ? date.map(o => utils.format(o, format)).join(formatSeperator) : invalidLabel; }; @@ -43,25 +45,27 @@ const getError = (value, props) => { invalidDateMessage, } = props; - if (!utils.isValid(value)) { + if (!value.every(utils.isValid)) { // if null - do not show error - if (utils.isNull(value)) { + if (value.every(utils.isNull)) { return ''; } return invalidDateMessage; } + let endOfDay = utils.endOfDay(utils.date()) if ( - (maxDate && utils.isAfter(value, maxDate)) || - (disableFuture && utils.isAfter(value, utils.endOfDay(utils.date()))) + (maxDate && value.some(o => utils.isAfter(o, maxDate))) || + (disableFuture && value.some(o => utils.isAfter(o, endOfDay))) ) { return maxDateMessage; } + let startOfDay = utils.startOfDay(utils.date()) if ( - (minDate && utils.isBefore(value, minDate)) || - (disablePast && utils.isBefore(value, utils.startOfDay(utils.date()))) + (minDate && value.some(o => utils.isBefore(o, minDate))) || + (disablePast && value.some(o => utils.isBefore(o, startOfDay))) ) { return minDateMessage; } @@ -71,19 +75,14 @@ const getError = (value, props) => { export class DateTextField extends PureComponent { static updateState = props => ({ - value: props.value, + value: props.utils.ensureArray(props.value), displayValue: getDisplayDate(props), - error: getError(props.utils.date(props.value), props), + error: getError(props.utils.ensureArray(props.value).map(props.utils.date), props), }); static propTypes = { classes: PropTypes.shape({}).isRequired, - value: PropTypes.oneOfType([ - PropTypes.object, - PropTypes.string, - PropTypes.number, - PropTypes.instanceOf(Date), - ]), + value: DomainPropTypes.dateRange, minDate: DomainPropTypes.date, maxDate: DomainPropTypes.date, disablePast: PropTypes.bool, @@ -125,6 +124,8 @@ export class DateTextField extends PureComponent { adornmentPosition: PropTypes.oneOf(['start', 'end']), /** Callback firing when date that applied in the keyboard is invalid */ onError: PropTypes.func, + /** String to join multiple values **/ + formatSeperator: PropTypes.string, /** Callback firing on change input in keyboard mode */ onInputChange: PropTypes.func, } @@ -133,7 +134,7 @@ export class DateTextField extends PureComponent { disabled: false, invalidLabel: 'Unknown', emptyLabel: '', - value: new Date(), + value: [ new Date() ], labelFunc: undefined, format: undefined, InputProps: undefined, @@ -156,6 +157,7 @@ export class DateTextField extends PureComponent { TextFieldComponent: TextField, InputAdornmentProps: {}, adornmentPosition: 'end', + formatSeperator: ', ', } state = DateTextField.updateState(this.props) @@ -181,6 +183,7 @@ export class DateTextField extends PureComponent { utils, format, onError, + formatSeperator, } = this.props; if (value === '') { @@ -193,8 +196,8 @@ export class DateTextField extends PureComponent { return; } - const oldValue = utils.date(this.state.value); - const newValue = utils.parse(value, format); + const oldValue = this.state.value.map(utils.date); + const newValue = value.split(formatSeperator).map(o => utils.parse(o, format)); const error = getError(newValue, this.props); this.setState({ @@ -203,11 +206,11 @@ export class DateTextField extends PureComponent { value: error ? newValue : oldValue, }, () => { if (!error && !utils.isEqual(newValue, oldValue)) { - this.props.onChange(newValue); + this.props.onChange(newValue.length > 1 ? newValue : newValue[0]); } if (error && onError) { - onError(newValue, error); + onError(newValue.length > 1 ? newValue : newValue[0], error); } }); } @@ -225,8 +228,8 @@ export class DateTextField extends PureComponent { }; handleChange = (e) => { - const { utils, format, onInputChange } = this.props; - const parsedValue = utils.parse(e.target.value, format); + const { utils, format, formatSeperator, onInputChange, } = this.props; + const parsedValue = e.target.value.split(formatSeperator).map(o => utils.parse(o, format)); if (onInputChange) { onInputChange(e); @@ -294,6 +297,7 @@ export class DateTextField extends PureComponent { TextFieldComponent, utils, value, + formatSeperator, onInputChange, ...other } = this.props; diff --git a/lib/src/constants/prop-types.js b/lib/src/constants/prop-types.js index 62f94687e..82e4da03e 100644 --- a/lib/src/constants/prop-types.js +++ b/lib/src/constants/prop-types.js @@ -7,7 +7,11 @@ const date = PropTypes.oneOfType([ PropTypes.instanceOf(Date), ]); -export default { +const dateRange = PropTypes.oneOfType([ date, -}; + PropTypes.arrayOf(date), +]); +export default { + date, dateRange +}; diff --git a/lib/src/utils/date-fns-utils.js b/lib/src/utils/date-fns-utils.js index b21bb7c20..d7a7814d2 100644 --- a/lib/src/utils/date-fns-utils.js +++ b/lib/src/utils/date-fns-utils.js @@ -60,9 +60,21 @@ export default class DateFnsUtils { return true; } + if (Array.isArray(date) || Array.isArray(comparing)) { + const dateArray = this.ensureArray(date); + const comparingArray = this.ensureArray(comparing); + + return dateArray.length == comparingArray.length && + dateArray.every((o, i) => isEqual(o, comparingArray[i])); + } + return isEqual(date, comparing); } + ensureArray (value) { + return Array.isArray(value) ? value : [ value ]; + } + addDays = addDays isValid = isValid @@ -77,6 +89,10 @@ export default class DateFnsUtils { isBefore = isBefore + isBetween (a, b, c) { + return a >= Math.min(b, c) && a <= Math.max(b, c) + } + isAfterDay(date, value) { return isAfter(date, endOfDay(value)); } diff --git a/lib/src/utils/luxon-utils.js b/lib/src/utils/luxon-utils.js index c3963eec3..60dc0bfbe 100644 --- a/lib/src/utils/luxon-utils.js +++ b/lib/src/utils/luxon-utils.js @@ -40,9 +40,21 @@ export default class LuxonUtils { return true; } + if (Array.isArray(date) || Array.isArray(comparing)) { + const dateArray = this.ensureArray(date); + const comparingArray = this.ensureArray(comparing); + + return dateArray.length == comparingArray.length && + dateArray.every((o, i) => this.isEqual(o, comparingArray[i])); + } + return value === comparing; } + ensureArray (value) { + return Array.isArray(value) ? value : [ value ]; + } + isSameDay(value, comparing) { return value.hasSame(comparing, 'day'); } @@ -51,6 +63,10 @@ export default class LuxonUtils { return value > comparing; } + isBetween (a, b, c) { + return a >= Math.min(b, c) && a <= Math.max(b, c) + } + isAfterDay(value, comparing) { const diff = value.diff(comparing, 'days').toObject(); return diff.days > 0; diff --git a/lib/src/utils/moment-utils.js b/lib/src/utils/moment-utils.js index 9d8c206b4..2c5ea3ac1 100644 --- a/lib/src/utils/moment-utils.js +++ b/lib/src/utils/moment-utils.js @@ -31,6 +31,10 @@ export default class MomentUtils { return date.isAfter(value); } + isBetween (a, b, c) { + return a >= Math.min(b, c) && a <= Math.max(b, c) + } + isBefore(date, value) { return date.isBefore(value); } @@ -142,9 +146,21 @@ export default class MomentUtils { return true; } + if (Array.isArray(date) || Array.isArray(comparing)) { + const dateArray = this.ensureArray(date); + const comparingArray = this.ensureArray(comparing); + + return dateArray.length == comparingArray.length && + dateArray.every((o, i) => this.isEqual(o, comparingArray[i])); + } + return this.moment(value).isSame(comparing); } + ensureArray (value) { + return Array.isArray(value) ? value : [ value ]; + } + getWeekArray(date) { const start = date.clone().startOf('month').startOf('week'); const end = date.clone().endOf('month').endOf('week'); diff --git a/lib/src/wrappers/ModalWrapper.jsx b/lib/src/wrappers/ModalWrapper.jsx index 199e02c9d..87ee46190 100644 --- a/lib/src/wrappers/ModalWrapper.jsx +++ b/lib/src/wrappers/ModalWrapper.jsx @@ -8,7 +8,7 @@ import DomainPropTypes from '../constants/prop-types'; export default class ModalWrapper extends PureComponent { static propTypes = { /** Picker value */ - value: DomainPropTypes.date, + value: DomainPropTypes.dateRange, /** Format string */ invalidLabel: PropTypes.node, /** Function for dynamic rendering label (date, invalidLabel) => string */ @@ -42,12 +42,13 @@ export default class ModalWrapper extends PureComponent { dialogContentClassName: PropTypes.string, isAccepted: PropTypes.bool.isRequired, container: PropTypes.node, + formatSeperator: PropTypes.string, } static defaultProps = { dialogContentClassName: '', invalidLabel: undefined, - value: new Date(), + value: [ new Date() ], labelFunc: undefined, okLabel: 'OK', cancelLabel: 'Cancel', @@ -63,6 +64,7 @@ export default class ModalWrapper extends PureComponent { onClose: undefined, onSetToday: undefined, container: undefined, + formatSeperator: ', ', } state = { @@ -156,6 +158,7 @@ export default class ModalWrapper extends PureComponent { onSetToday, isAccepted, container, + formatSeperator, ...other } = this.props; @@ -169,6 +172,7 @@ export default class ModalWrapper extends PureComponent { invalidLabel={invalidLabel} labelFunc={labelFunc} clearable={clearable} + formatSeperator={formatSeperator} {...other} />