@blueprintjs/datetime
Version:
Components for interacting with dates and times
376 lines • 20.7 kB
JavaScript
"use strict";
/*
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DateRangePicker = void 0;
const tslib_1 = require("tslib");
const classnames_1 = tslib_1.__importDefault(require("classnames"));
const date_fns_1 = require("date-fns");
const React = tslib_1.__importStar(require("react"));
const core_1 = require("@blueprintjs/core");
const common_1 = require("../../common");
const classes_1 = require("../../common/classes");
const dateRangeSelectionStrategy_1 = require("../../common/dateRangeSelectionStrategy");
const dayPickerModifiers_1 = require("../../common/dayPickerModifiers");
const monthAndYear_1 = require("../../common/monthAndYear");
const datePickerContext_1 = require("../date-picker/datePickerContext");
const datePickerUtils_1 = require("../date-picker/datePickerUtils");
const dateFnsLocalizedComponent_1 = require("../dateFnsLocalizedComponent");
const shortcuts_1 = require("../shortcuts/shortcuts");
const timePicker_1 = require("../time-picker/timePicker");
const contiguousDayRangePicker_1 = require("./contiguousDayRangePicker");
const nonContiguousDayRangePicker_1 = require("./nonContiguousDayRangePicker");
const NULL_RANGE = [null, null];
/**
* Date range picker component.
*
* @see https://blueprintjs.com/docs/#datetime/date-range-picker
*/
class DateRangePicker extends dateFnsLocalizedComponent_1.DateFnsLocalizedComponent {
constructor(props) {
var _a;
super(props);
// these will get merged with the user's own
this.modifiers = {
[dayPickerModifiers_1.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 common_1.DateUtils.isDayInRange(day, hoverValue, true);
},
[`${dayPickerModifiers_1.HOVERED_RANGE_MODIFIER}-start`]: day => {
const { hoverValue } = this.state;
if (hoverValue == null || hoverValue[0] == null) {
return false;
}
return common_1.DateUtils.isSameDay(hoverValue[0], day);
},
[`${dayPickerModifiers_1.HOVERED_RANGE_MODIFIER}-end`]: day => {
const { hoverValue } = this.state;
if (hoverValue == null || hoverValue[1] == null) {
return false;
}
return common_1.DateUtils.isSameDay(hoverValue[1], day);
},
};
this.modifiersClassNames = {
[dayPickerModifiers_1.HOVERED_RANGE_MODIFIER]: common_1.Classes.DATERANGEPICKER3_HOVERED_RANGE,
[`${dayPickerModifiers_1.HOVERED_RANGE_MODIFIER}-start`]: common_1.Classes.DATERANGEPICKER3_HOVERED_RANGE_START,
[`${dayPickerModifiers_1.HOVERED_RANGE_MODIFIER}-end`]: common_1.Classes.DATERANGEPICKER3_HOVERED_RANGE_END,
};
this.initialMonthAndYear = (_a = monthAndYear_1.MonthAndYear.fromDate(new Date())) !== null && _a !== void 0 ? _a : new monthAndYear_1.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 = common_1.DateUtils.getDateTime(value[dateIndex] != null ? common_1.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 (0, date_fns_1.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 => (0, date_fns_1.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_1.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] = common_1.DateUtils.getDateTime(selectedRange[0], selectedTimeRange[0]);
selectedRange[1] = common_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_1.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 = (0, classnames_1.default)(common_1.Classes.DATEPICKER, common_1.Classes.DATERANGEPICKER, className, {
[common_1.Classes.DATEPICKER3_HIGHLIGHT_CURRENT_DAY]: this.props.highlightCurrentDay,
[common_1.Classes.DATERANGEPICKER_CONTIGUOUS]: contiguousCalendarMonths,
[common_1.Classes.DATERANGEPICKER_SINGLE_MONTH]: isSingleMonthOnly,
[common_1.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: common_1.Classes.DATEPICKER_CONTENT },
React.createElement(datePickerContext_1.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_1.MonthAndYear(initialMonth.getMonth(), initialMonth.getFullYear());
}
if (isControlled &&
(!common_1.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 && !common_1.DateUtils.isDayRangeInRange(defaultValue, dateRange)) {
console.error(common_1.Errors.DATERANGEPICKER_DEFAULT_VALUE_INVALID);
}
if (initialMonth != null && !common_1.DateUtils.isMonthInRange(initialMonth, dateRange)) {
console.error(common_1.Errors.DATERANGEPICKER_INITIAL_MONTH_INVALID);
}
if (maxDate != null && minDate != null && maxDate < minDate && !common_1.DateUtils.isSameDay(maxDate, minDate)) {
console.error(common_1.Errors.DATERANGEPICKER_MAX_DATE_INVALID);
}
if (value != null && !common_1.DateUtils.isDayRangeInRange(value, dateRange)) {
console.error(common_1.Errors.DATERANGEPICKER_VALUE_INVALID);
}
if (boundaryToModify != null && boundaryToModify !== core_1.Boundary.START && boundaryToModify !== core_1.Boundary.END) {
console.error(common_1.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_1.DatePickerUtils.getDefaultMaxDate(), minDate = datePickerUtils_1.DatePickerUtils.getDefaultMinDate(), timePrecision, } = this.props;
return [
React.createElement(shortcuts_1.DatePickerShortcutMenu, { key: "shortcuts", allowSingleDayRange: allowSingleDayRange, maxDate: maxDate, minDate: minDate, onShortcutClick: this.handleShortcutClick, selectedShortcutIndex: selectedShortcutIndex, shortcuts: shortcuts, timePrecision: timePrecision }),
React.createElement(core_1.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 === common_1.TimePrecision.SECOND ||
timePrecision === common_1.TimePrecision.MILLISECOND;
return (React.createElement("div", { className: (0, classnames_1.default)(common_1.Classes.DATERANGEPICKER_TIMEPICKERS, {
[common_1.Classes.DATERANGEPICKER3_TIMEPICKERS_STACKED]: isShowingOneMonth && isLongTimePicker,
}) },
React.createElement(timePicker_1.TimePicker, { precision: timePrecision, ...timePickerProps, onChange: this.handleTimeChangeLeftCalendar, value: this.state.time[0] }),
React.createElement(timePicker_1.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_1.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_1.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: {
...classes_1.dayPickerClassNameOverrides,
...dayPickerProps.classNames,
},
formatters: {
formatWeekdayName: this.formatWeekdayName,
...dayPickerProps.formatters,
},
modifiers: (0, dayPickerModifiers_1.combineModifiers)(this.modifiers, dayPickerProps.modifiers),
modifiersClassNames: {
...this.modifiersClassNames,
...dayPickerProps.modifiersClassNames,
},
};
}
}
exports.DateRangePicker = DateRangePicker;
DateRangePicker.defaultProps = {
allowSingleDayRange: false,
contiguousCalendarMonths: true,
dayPickerProps: {},
locale: "en-US",
maxDate: datePickerUtils_1.DatePickerUtils.getDefaultMaxDate(),
minDate: datePickerUtils_1.DatePickerUtils.getDefaultMinDate(),
reverseMonthAndYearMenus: false,
shortcuts: true,
singleMonthOnly: false,
timePickerProps: {},
};
DateRangePicker.displayName = `${core_1.DISPLAYNAME_PREFIX}.DateRangePicker`;
function getIsSingleMonthOnly(props) {
return props.singleMonthOnly || common_1.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 && common_1.DateUtils.isSameMonth(props.initialMonth, props.maxDate)) {
// special case: if initial month is same as maxDate month, display it on the right calendar
return common_1.DateUtils.getDatePreviousMonth(props.initialMonth);
}
return props.initialMonth;
}
else if (value[0] != null) {
if (!isSingleMonthOnly && common_1.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 common_1.DateUtils.getDatePreviousMonth(value[0]);
}
return common_1.DateUtils.clone(value[0]);
}
else if (value[1] != null) {
const month = common_1.DateUtils.clone(value[1]);
if (!common_1.DateUtils.isSameMonth(month, props.minDate)) {
month.setMonth(month.getMonth() - 1);
}
return month;
}
else if (common_1.DateUtils.isDayInRange(today, [props.minDate, props.maxDate])) {
if (!isSingleMonthOnly && common_1.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 = common_1.DateUtils.getDateBetween([props.minDate, props.maxDate]);
if (!isSingleMonthOnly && common_1.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