@blueprintjs/datetime
Version:
Components for interacting with dates and times
371 lines • 19.6 kB
JavaScript
/*
* Copyright 2023 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from "classnames";
import { addDays, format } from "date-fns";
import * as React from "react";
import { Boundary, DISPLAYNAME_PREFIX, Divider } from "@blueprintjs/core";
import { Classes, DateUtils, Errors, TimePrecision } from "../../common";
import { dayPickerClassNameOverrides } from "../../common/classes";
import { DateRangeSelectionStrategy } from "../../common/dateRangeSelectionStrategy";
import { combineModifiers, HOVERED_RANGE_MODIFIER } from "../../common/dayPickerModifiers";
import { MonthAndYear } from "../../common/monthAndYear";
import { DatePickerProvider } from "../date-picker/datePickerContext";
import { DatePickerUtils } from "../date-picker/datePickerUtils";
import { DateFnsLocalizedComponent } from "../dateFnsLocalizedComponent";
import { DatePickerShortcutMenu } from "../shortcuts/shortcuts";
import { TimePicker } from "../time-picker/timePicker";
import { ContiguousDayRangePicker } from "./contiguousDayRangePicker";
import { NonContiguousDayRangePicker } from "./nonContiguousDayRangePicker";
const NULL_RANGE = [null, null];
/**
* Date range picker component.
*
* @see https://blueprintjs.com/docs/#datetime/date-range-picker
*/
export class DateRangePicker extends DateFnsLocalizedComponent {
constructor(props) {
var _a;
super(props);
// these will get merged with the user's own
this.modifiers = {
[HOVERED_RANGE_MODIFIER]: day => {
const { hoverValue, value: [selectedStart, selectedEnd], } = this.state;
if (selectedStart == null && selectedEnd == null) {
return false;
}
if (hoverValue == null || hoverValue[0] == null || hoverValue[1] == null) {
return false;
}
return DateUtils.isDayInRange(day, hoverValue, true);
},
[`${HOVERED_RANGE_MODIFIER}-start`]: day => {
const { hoverValue } = this.state;
if (hoverValue == null || hoverValue[0] == null) {
return false;
}
return DateUtils.isSameDay(hoverValue[0], day);
},
[`${HOVERED_RANGE_MODIFIER}-end`]: day => {
const { hoverValue } = this.state;
if (hoverValue == null || hoverValue[1] == null) {
return false;
}
return DateUtils.isSameDay(hoverValue[1], day);
},
};
this.modifiersClassNames = {
[HOVERED_RANGE_MODIFIER]: Classes.DATERANGEPICKER3_HOVERED_RANGE,
[`${HOVERED_RANGE_MODIFIER}-start`]: Classes.DATERANGEPICKER3_HOVERED_RANGE_START,
[`${HOVERED_RANGE_MODIFIER}-end`]: Classes.DATERANGEPICKER3_HOVERED_RANGE_END,
};
this.initialMonthAndYear = (_a = MonthAndYear.fromDate(new Date())) !== null && _a !== void 0 ? _a : new MonthAndYear(new Date().getMonth(), new Date().getFullYear());
this.handleTimeChange = (newTime, dateIndex) => {
var _a, _b, _c, _d;
(_b = (_a = this.props.timePickerProps) === null || _a === void 0 ? void 0 : _a.onChange) === null || _b === void 0 ? void 0 : _b.call(_a, newTime);
const { value, time } = this.state;
const newValue = DateUtils.getDateTime(value[dateIndex] != null ? DateUtils.clone(value[dateIndex]) : this.getDefaultDate(dateIndex), newTime);
const newDateRange = [value[0], value[1]];
newDateRange[dateIndex] = newValue;
const newTimeRange = [time[0], time[1]];
newTimeRange[dateIndex] = newTime;
(_d = (_c = this.props).onChange) === null || _d === void 0 ? void 0 : _d.call(_c, newDateRange);
this.setState({ time: newTimeRange, value: newDateRange });
};
// When a user sets the time value before choosing a date, we need to pick a date for them
// The default depends on the value of the other date since there's an invariant
// that the left/0 date is always less than the right/1 date
this.getDefaultDate = (dateIndex) => {
const { value } = this.state;
const otherIndex = dateIndex === 0 ? 1 : 0;
const otherDate = value[otherIndex];
if (otherDate == null) {
return new Date();
}
const { allowSingleDayRange } = this.props;
if (!allowSingleDayRange) {
const dateDiff = dateIndex === 0 ? -1 : 1;
return addDays(otherDate, dateDiff);
}
return otherDate;
};
this.handleTimeChangeLeftCalendar = (time) => {
this.handleTimeChange(time, 0);
};
this.handleTimeChangeRightCalendar = (time) => {
this.handleTimeChange(time, 1);
};
/**
* Custom formatter to render weekday names in the calendar header. The default formatter generally works fine,
* but it was returning CAPITALIZED strings for some reason, while we prefer Title Case.
*/
this.formatWeekdayName = date => format(date, "EEEEEE", { locale: this.state.locale });
this.handleDayMouseEnter = (day, activeModifiers, e) => {
var _a, _b, _c, _d;
(_b = (_a = this.props.dayPickerProps) === null || _a === void 0 ? void 0 : _a.onDayMouseEnter) === null || _b === void 0 ? void 0 : _b.call(_a, day, activeModifiers, e);
if (activeModifiers.disabled) {
return;
}
const { dateRange, boundary } = DateRangeSelectionStrategy.getNextState(this.state.value, day, this.props.allowSingleDayRange, this.props.boundaryToModify);
this.setState({ hoverValue: dateRange });
(_d = (_c = this.props).onHoverChange) === null || _d === void 0 ? void 0 : _d.call(_c, dateRange, day, boundary);
};
this.handleDayMouseLeave = (day, activeModifiers, e) => {
var _a, _b, _c, _d;
(_b = (_a = this.props.dayPickerProps) === null || _a === void 0 ? void 0 : _a.onDayMouseLeave) === null || _b === void 0 ? void 0 : _b.call(_a, day, activeModifiers, e);
if (activeModifiers.disabled) {
return;
}
this.setState({ hoverValue: undefined });
(_d = (_c = this.props).onHoverChange) === null || _d === void 0 ? void 0 : _d.call(_c, undefined, day, undefined);
};
this.handleDayRangeSelect = (nextValue, selectedDay, boundary) => {
var _a, _b;
// update the hovered date range after click to show the newly selected
// state, at leasts until the mouse moves again
this.setState({ hoverValue: nextValue });
(_b = (_a = this.props).onHoverChange) === null || _b === void 0 ? void 0 : _b.call(_a, nextValue, selectedDay, boundary);
this.updateSelectedRange(nextValue);
};
this.handleShortcutClick = (shortcut, selectedShortcutIndex) => {
var _a, _b;
const { dateRange, includeTime } = shortcut;
if (includeTime) {
this.updateSelectedRange(dateRange, [dateRange[0], dateRange[1]]);
}
else {
this.updateSelectedRange(dateRange);
}
if (this.props.selectedShortcutIndex === undefined) {
// uncontrolled shorcut selection
this.setState({ selectedShortcutIndex });
}
(_b = (_a = this.props).onShortcutChange) === null || _b === void 0 ? void 0 : _b.call(_a, shortcut, selectedShortcutIndex);
};
this.updateSelectedRange = (selectedRange, selectedTimeRange = this.state.time) => {
var _a, _b;
selectedRange[0] = DateUtils.getDateTime(selectedRange[0], selectedTimeRange[0]);
selectedRange[1] = DateUtils.getDateTime(selectedRange[1], selectedTimeRange[1]);
if (this.props.value == null) {
// uncontrolled range selection
this.setState({ time: selectedTimeRange, value: selectedRange });
}
(_b = (_a = this.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, selectedRange);
};
const value = getInitialValue(props);
const time = value;
const initialMonth = getInitialMonth(props, value);
this.initialMonthAndYear = new MonthAndYear(initialMonth.getMonth(), initialMonth.getFullYear());
this.state = {
hoverValue: NULL_RANGE,
locale: undefined,
selectedShortcutIndex: this.props.selectedShortcutIndex !== undefined ? this.props.selectedShortcutIndex : -1,
time,
value,
};
}
render() {
const { className, contiguousCalendarMonths, footerElement } = this.props;
const isSingleMonthOnly = getIsSingleMonthOnly(this.props);
const classes = classNames(Classes.DATEPICKER, Classes.DATERANGEPICKER, className, {
[Classes.DATEPICKER3_HIGHLIGHT_CURRENT_DAY]: this.props.highlightCurrentDay,
[Classes.DATERANGEPICKER_CONTIGUOUS]: contiguousCalendarMonths,
[Classes.DATERANGEPICKER_SINGLE_MONTH]: isSingleMonthOnly,
[Classes.DATERANGEPICKER3_REVERSE_MONTH_AND_YEAR]: this.props.reverseMonthAndYearMenus,
});
// use the left DayPicker when we only need one
return (React.createElement("div", { className: classes },
this.maybeRenderShortcuts(),
React.createElement("div", { className: Classes.DATEPICKER_CONTENT },
React.createElement(DatePickerProvider, { ...this.props, ...this.state },
contiguousCalendarMonths || isSingleMonthOnly
? this.renderContiguousDayRangePicker(isSingleMonthOnly)
: this.renderNonContiguousDayRangePicker(),
this.maybeRenderTimePickers(isSingleMonthOnly),
footerElement))));
}
async componentDidMount() {
await super.componentDidMount();
}
async componentDidUpdate(prevProps) {
var _a;
super.componentDidUpdate(prevProps);
const isControlled = prevProps.value !== undefined && this.props.value !== undefined;
if (prevProps.contiguousCalendarMonths !== this.props.contiguousCalendarMonths) {
const initialMonth = getInitialMonth(this.props, getInitialValue(this.props));
this.initialMonthAndYear = new MonthAndYear(initialMonth.getMonth(), initialMonth.getFullYear());
}
if (isControlled &&
(!DateUtils.areRangesEqual(prevProps.value, this.props.value) ||
prevProps.contiguousCalendarMonths !== this.props.contiguousCalendarMonths)) {
this.setState({ value: (_a = this.props.value) !== null && _a !== void 0 ? _a : NULL_RANGE });
}
if (this.props.selectedShortcutIndex !== prevProps.selectedShortcutIndex) {
this.setState({ selectedShortcutIndex: this.props.selectedShortcutIndex });
}
}
validateProps(props) {
const { defaultValue, initialMonth, maxDate, minDate, boundaryToModify, value } = props;
const dateRange = [minDate, maxDate];
if (defaultValue != null && !DateUtils.isDayRangeInRange(defaultValue, dateRange)) {
console.error(Errors.DATERANGEPICKER_DEFAULT_VALUE_INVALID);
}
if (initialMonth != null && !DateUtils.isMonthInRange(initialMonth, dateRange)) {
console.error(Errors.DATERANGEPICKER_INITIAL_MONTH_INVALID);
}
if (maxDate != null && minDate != null && maxDate < minDate && !DateUtils.isSameDay(maxDate, minDate)) {
console.error(Errors.DATERANGEPICKER_MAX_DATE_INVALID);
}
if (value != null && !DateUtils.isDayRangeInRange(value, dateRange)) {
console.error(Errors.DATERANGEPICKER_VALUE_INVALID);
}
if (boundaryToModify != null && boundaryToModify !== Boundary.START && boundaryToModify !== Boundary.END) {
console.error(Errors.DATERANGEPICKER_PREFERRED_BOUNDARY_TO_MODIFY_INVALID);
}
}
maybeRenderShortcuts() {
const { shortcuts } = this.props;
if (shortcuts == null || shortcuts === false) {
return null;
}
const { selectedShortcutIndex } = this.state;
const { allowSingleDayRange, maxDate = DatePickerUtils.getDefaultMaxDate(), minDate = DatePickerUtils.getDefaultMinDate(), timePrecision, } = this.props;
return [
React.createElement(DatePickerShortcutMenu, { key: "shortcuts", allowSingleDayRange: allowSingleDayRange, maxDate: maxDate, minDate: minDate, onShortcutClick: this.handleShortcutClick, selectedShortcutIndex: selectedShortcutIndex, shortcuts: shortcuts, timePrecision: timePrecision }),
React.createElement(Divider, { key: "div" }),
];
}
maybeRenderTimePickers(isShowingOneMonth) {
// timePrecision may be set as a root prop or as a property inside timePickerProps, so we need to check both
const { timePickerProps, timePrecision = timePickerProps === null || timePickerProps === void 0 ? void 0 : timePickerProps.precision } = this.props;
if (timePrecision == null && timePickerProps === DateRangePicker.defaultProps.timePickerProps) {
return null;
}
const isLongTimePicker = (timePickerProps === null || timePickerProps === void 0 ? void 0 : timePickerProps.useAmPm) ||
timePrecision === TimePrecision.SECOND ||
timePrecision === TimePrecision.MILLISECOND;
return (React.createElement("div", { className: classNames(Classes.DATERANGEPICKER_TIMEPICKERS, {
[Classes.DATERANGEPICKER3_TIMEPICKERS_STACKED]: isShowingOneMonth && isLongTimePicker,
}) },
React.createElement(TimePicker, { precision: timePrecision, ...timePickerProps, onChange: this.handleTimeChangeLeftCalendar, value: this.state.time[0] }),
React.createElement(TimePicker, { precision: timePrecision, ...timePickerProps, onChange: this.handleTimeChangeRightCalendar, value: this.state.time[1] })));
}
/**
* Render a standard day range picker where props.contiguousCalendarMonths is expected to be `true`.
*/
renderContiguousDayRangePicker(singleMonthOnly) {
const { dayPickerProps, ...props } = this.props;
return (React.createElement(ContiguousDayRangePicker, { ...props, contiguousCalendarMonths: true, dayPickerEventHandlers: {
onDayMouseEnter: this.handleDayMouseEnter,
onDayMouseLeave: this.handleDayMouseLeave,
}, dayPickerProps: this.resolvedDayPickerProps, initialMonthAndYear: this.initialMonthAndYear, locale: this.state.locale, onRangeSelect: this.handleDayRangeSelect, singleMonthOnly: singleMonthOnly, value: this.state.value }));
}
/**
* react-day-picker doesn't have built-in support for non-contiguous calendar months in its range picker,
* so we have to implement this ourselves.
*/
renderNonContiguousDayRangePicker() {
const { dayPickerProps, ...props } = this.props;
return (React.createElement(NonContiguousDayRangePicker, { ...props, dayPickerProps: this.resolvedDayPickerProps, dayPickerEventHandlers: {
onDayMouseEnter: this.handleDayMouseEnter,
onDayMouseLeave: this.handleDayMouseLeave,
}, initialMonthAndYear: this.initialMonthAndYear, locale: this.state.locale, onRangeSelect: this.handleDayRangeSelect, value: this.state.value }));
}
get resolvedDayPickerProps() {
const { dayPickerProps = {} } = this.props;
return {
...dayPickerProps,
classNames: {
...dayPickerClassNameOverrides,
...dayPickerProps.classNames,
},
formatters: {
formatWeekdayName: this.formatWeekdayName,
...dayPickerProps.formatters,
},
modifiers: combineModifiers(this.modifiers, dayPickerProps.modifiers),
modifiersClassNames: {
...this.modifiersClassNames,
...dayPickerProps.modifiersClassNames,
},
};
}
}
DateRangePicker.defaultProps = {
allowSingleDayRange: false,
contiguousCalendarMonths: true,
dayPickerProps: {},
locale: "en-US",
maxDate: DatePickerUtils.getDefaultMaxDate(),
minDate: DatePickerUtils.getDefaultMinDate(),
reverseMonthAndYearMenus: false,
shortcuts: true,
singleMonthOnly: false,
timePickerProps: {},
};
DateRangePicker.displayName = `${DISPLAYNAME_PREFIX}.DateRangePicker`;
function getIsSingleMonthOnly(props) {
return props.singleMonthOnly || DateUtils.isSameMonth(props.minDate, props.maxDate);
}
function getInitialValue(props) {
if (props.value != null) {
return props.value;
}
if (props.defaultValue != null) {
return props.defaultValue;
}
return NULL_RANGE;
}
function getInitialMonth(props, value) {
const today = new Date();
const isSingleMonthOnly = getIsSingleMonthOnly(props);
if (props.initialMonth != null) {
if (!isSingleMonthOnly && DateUtils.isSameMonth(props.initialMonth, props.maxDate)) {
// special case: if initial month is same as maxDate month, display it on the right calendar
return DateUtils.getDatePreviousMonth(props.initialMonth);
}
return props.initialMonth;
}
else if (value[0] != null) {
if (!isSingleMonthOnly && DateUtils.isSameMonth(value[0], props.maxDate)) {
// special case: if start of range is selected and that date is in the maxDate month, display it on the right calendar
return DateUtils.getDatePreviousMonth(value[0]);
}
return DateUtils.clone(value[0]);
}
else if (value[1] != null) {
const month = DateUtils.clone(value[1]);
if (!DateUtils.isSameMonth(month, props.minDate)) {
month.setMonth(month.getMonth() - 1);
}
return month;
}
else if (DateUtils.isDayInRange(today, [props.minDate, props.maxDate])) {
if (!isSingleMonthOnly && DateUtils.isSameMonth(today, props.maxDate)) {
// special case: if today is in the maxDate month, display it on the right calendar
today.setMonth(today.getMonth() - 1);
}
return today;
}
else {
const betweenDate = DateUtils.getDateBetween([props.minDate, props.maxDate]);
if (!isSingleMonthOnly && DateUtils.isSameMonth(betweenDate, props.maxDate)) {
// special case: if betweenDate is in the maxDate month, display it on the right calendar
betweenDate.setMonth(betweenDate.getMonth() - 1);
}
return betweenDate;
}
}
//# sourceMappingURL=dateRangePicker.js.map