UNPKG

@blueprintjs/datetime

Version:

Components for interacting with dates and times

376 lines 20.7 kB
"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