@blueprintjs/datetime
Version:
Components for interacting with dates and times
322 lines • 16.6 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.DatePicker = 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 react_day_picker_1 = require("react-day-picker");
const core_1 = require("@blueprintjs/core");
const common_1 = require("../../common");
const classes_1 = require("../../common/classes");
const dateFnsLocalizedComponent_1 = require("../dateFnsLocalizedComponent");
const datePickerDropdown_1 = require("../react-day-picker/datePickerDropdown");
const datePickerNavIcons_1 = require("../react-day-picker/datePickerNavIcons");
const shortcuts_1 = require("../shortcuts/shortcuts");
const timePicker_1 = require("../time-picker/timePicker");
const datePickerContext_1 = require("./datePickerContext");
const datePickerUtils_1 = require("./datePickerUtils");
/**
* Date picker component.
*
* @see https://blueprintjs.com/docs/#datetime/date-picker
*/
class DatePicker extends dateFnsLocalizedComponent_1.DateFnsLocalizedComponent {
constructor(props) {
super(props);
this.ignoreNextMonthChange = false;
/**
* 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.renderWeekdayName = date => {
return (0, date_fns_1.format)(date, "EEEEEE", { locale: this.state.locale });
};
this.handleDaySelect = (day, selectedDay, activeModifiers, e) => {
var _a, _b;
if (activeModifiers.disabled) {
return;
}
else if (day === undefined) {
this.handleClearClick();
return;
}
this.updateDay(day);
(_b = (_a = this.props.dayPickerProps) === null || _a === void 0 ? void 0 : _a.onSelect) === null || _b === void 0 ? void 0 : _b.call(_a, day, selectedDay, activeModifiers, e);
// allow toggling selected date by clicking it again (if prop enabled)
const newValue = this.props.canClearSelection && activeModifiers.selected
? null
: common_1.DateUtils.getDateTime(day, this.state.value);
this.updateValue(newValue, true);
};
this.handleShortcutClick = (shortcut, selectedShortcutIndex) => {
const { onShortcutChange, selectedShortcutIndex: currentShortcutIndex } = this.props;
const { dateRange, includeTime } = shortcut;
const newDate = dateRange[0];
const newValue = includeTime ? newDate : common_1.DateUtils.getDateTime(newDate, this.state.value);
if (newDate == null) {
return;
}
this.updateDay(newDate);
this.updateValue(newValue, true);
if (currentShortcutIndex === undefined) {
this.setState({ selectedShortcutIndex });
}
const datePickerShortcut = { ...shortcut, date: newDate };
onShortcutChange === null || onShortcutChange === void 0 ? void 0 : onShortcutChange(datePickerShortcut, selectedShortcutIndex);
};
this.updateDay = (day) => {
if (this.props.value === undefined) {
// set now if uncontrolled, otherwise they'll be updated in `componentDidUpdate`
this.setState({
displayMonth: day.getMonth(),
displayYear: day.getFullYear(),
selectedDay: day.getDate(),
});
}
if (this.state.value != null && this.state.value.getMonth() !== day.getMonth()) {
this.ignoreNextMonthChange = true;
}
};
this.handleClearClick = () => this.updateValue(null, true);
this.handleMonthChange = (newDate) => {
var _a, _b;
const date = this.computeValidDateInSpecifiedMonthYear(newDate.getFullYear(), newDate.getMonth());
if (date != null) {
this.setState({ displayMonth: date.getMonth(), displayYear: date.getFullYear() });
(_b = (_a = this.props.dayPickerProps) === null || _a === void 0 ? void 0 : _a.onMonthChange) === null || _b === void 0 ? void 0 : _b.call(_a, date);
}
if (this.state.value !== null) {
// if handleDayClick just got run (so this flag is set), then the
// user selected a date in a new month, so don't invoke onChange a
// second time
this.updateValue(date, false, this.ignoreNextMonthChange);
this.ignoreNextMonthChange = false;
}
};
this.handleTodayClick = () => {
const { timezone } = this.props;
const today = new Date();
const value = timezone != null ? common_1.TimezoneUtils.convertLocalDateToTimezoneTime(today, timezone) : today;
const displayMonth = value.getMonth();
const displayYear = value.getFullYear();
const selectedDay = value.getDate();
this.setState({ displayMonth, displayYear, selectedDay });
this.updateValue(value, true);
};
this.handleTimeChange = (time) => {
var _a, _b;
(_b = (_a = this.props.timePickerProps) === null || _a === void 0 ? void 0 : _a.onChange) === null || _b === void 0 ? void 0 : _b.call(_a, time);
const { value } = this.state;
const newValue = common_1.DateUtils.getDateTime(value != null ? value : new Date(), time);
this.updateValue(newValue, true);
};
const value = getInitialValue(props);
const initialMonth = getInitialMonth(props, value);
this.state = {
displayMonth: initialMonth.getMonth(),
displayYear: initialMonth.getFullYear(),
locale: undefined,
selectedDay: value == null ? null : value.getDate(),
selectedShortcutIndex: this.props.selectedShortcutIndex !== undefined ? this.props.selectedShortcutIndex : -1,
value,
};
}
render() {
var _a;
const { className, dayPickerProps, footerElement, maxDate, minDate, showActionsBar } = this.props;
const { displayMonth, displayYear, locale } = this.state;
return (React.createElement("div", { className: (0, classnames_1.default)(common_1.Classes.DATEPICKER, className, {
[common_1.Classes.DATEPICKER3_HIGHLIGHT_CURRENT_DAY]: this.props.highlightCurrentDay,
[common_1.Classes.DATEPICKER3_REVERSE_MONTH_AND_YEAR]: this.props.reverseMonthAndYearMenus,
}) },
this.maybeRenderShortcuts(),
React.createElement("div", { className: common_1.Classes.DATEPICKER_CONTENT },
React.createElement(datePickerContext_1.DatePickerProvider, { ...this.props, ...this.state },
React.createElement(react_day_picker_1.DayPicker, { locale: locale, showOutsideDays: true, ...dayPickerProps, captionLayout: "dropdown-buttons", classNames: {
...classes_1.dayPickerClassNameOverrides,
...dayPickerProps === null || dayPickerProps === void 0 ? void 0 : dayPickerProps.classNames,
}, components: {
Dropdown: datePickerDropdown_1.DatePickerDropdown,
IconLeft: datePickerNavIcons_1.IconLeft,
IconRight: datePickerNavIcons_1.IconRight,
...dayPickerProps === null || dayPickerProps === void 0 ? void 0 : dayPickerProps.components,
}, formatters: {
formatWeekdayName: this.renderWeekdayName,
...dayPickerProps === null || dayPickerProps === void 0 ? void 0 : dayPickerProps.formatters,
}, fromDate: minDate, mode: "single", month: new Date(displayYear, displayMonth), onMonthChange: this.handleMonthChange, onSelect: this.handleDaySelect, required: !this.props.canClearSelection, selected: (_a = this.state.value) !== null && _a !== void 0 ? _a : undefined, toDate: maxDate }),
this.maybeRenderTimePicker(),
showActionsBar && this.renderOptionsBar(),
footerElement))));
}
async componentDidMount() {
await super.componentDidMount();
}
async componentDidUpdate(prevProps) {
super.componentDidUpdate(prevProps);
if (this.props.value !== prevProps.value) {
if (this.props.value == null) {
// clear the value
this.setState({ value: null });
}
else {
this.setState({
displayMonth: this.props.value.getMonth(),
displayYear: this.props.value.getFullYear(),
selectedDay: this.props.value.getDate(),
value: this.props.value,
});
}
}
if (this.props.selectedShortcutIndex !== prevProps.selectedShortcutIndex) {
this.setState({ selectedShortcutIndex: this.props.selectedShortcutIndex });
}
}
validateProps(props) {
const { defaultValue, initialMonth, maxDate, minDate, value } = props;
if (defaultValue != null && !common_1.DateUtils.isDayInRange(defaultValue, [minDate, maxDate])) {
console.error(common_1.Errors.DATEPICKER_DEFAULT_VALUE_INVALID);
}
if (initialMonth != null && !common_1.DateUtils.isMonthInRange(initialMonth, [minDate, maxDate])) {
console.error(common_1.Errors.DATEPICKER_INITIAL_MONTH_INVALID);
}
if (maxDate != null && minDate != null && maxDate < minDate && !common_1.DateUtils.isSameDay(maxDate, minDate)) {
console.error(common_1.Errors.DATEPICKER_MAX_DATE_INVALID);
}
if (value != null && !common_1.DateUtils.isDayInRange(value, [minDate, maxDate])) {
console.error(common_1.Errors.DATEPICKER_VALUE_INVALID);
}
}
renderOptionsBar() {
const { clearButtonText, todayButtonText, minDate, maxDate, canClearSelection } = this.props;
const todayEnabled = isTodayEnabled(minDate, maxDate);
return [
React.createElement(core_1.Divider, { key: "div" }),
React.createElement("div", { className: common_1.Classes.DATEPICKER_FOOTER, key: "footer" },
React.createElement(core_1.Button, { disabled: !todayEnabled, onClick: this.handleTodayClick, text: todayButtonText, variant: "minimal" }),
React.createElement(core_1.Button, { disabled: !canClearSelection, onClick: this.handleClearClick, text: clearButtonText, variant: "minimal" })),
];
}
maybeRenderTimePicker() {
const { timePrecision, timePickerProps, minDate, maxDate } = this.props;
if (timePrecision == null && timePickerProps === undefined) {
return null;
}
const applyMin = this.state.value != null && common_1.DateUtils.isSameDay(this.state.value, minDate);
const applyMax = this.state.value != null && common_1.DateUtils.isSameDay(this.state.value, maxDate);
return (React.createElement("div", { className: common_1.Classes.DATEPICKER_TIMEPICKER_WRAPPER },
React.createElement(timePicker_1.TimePicker, { precision: timePrecision, minTime: applyMin ? minDate : undefined, maxTime: applyMax ? maxDate : undefined, ...timePickerProps, onChange: this.handleTimeChange, value: this.state.value })));
}
maybeRenderShortcuts() {
const { shortcuts } = this.props;
if (shortcuts == null || shortcuts === false) {
return null;
}
const { selectedShortcutIndex } = this.state;
const { maxDate = datePickerUtils_1.DatePickerUtils.getDefaultMaxDate(), minDate = datePickerUtils_1.DatePickerUtils.getDefaultMinDate(), timePrecision, } = this.props;
// Reuse the existing date range shortcuts and only care about start date
const dateRangeShortcuts = shortcuts === true
? true
: shortcuts.map(shortcut => ({
...shortcut,
// TODO: Remove cast after setting "strictNullChecks: true"
dateRange: [shortcut.date, null],
}));
return [
React.createElement(shortcuts_1.DatePickerShortcutMenu, { key: "shortcuts", allowSingleDayRange: true, maxDate: maxDate, minDate: minDate, selectedShortcutIndex: selectedShortcutIndex, shortcuts: dateRangeShortcuts, timePrecision: timePrecision, onShortcutClick: this.handleShortcutClick, useSingleDateShortcuts: true }),
React.createElement(core_1.Divider, { key: "div" }),
];
}
computeValidDateInSpecifiedMonthYear(displayYear, displayMonth) {
const { minDate, maxDate } = this.props;
const { selectedDay } = this.state;
// month is 0-based, date is 1-based. date 0 is last day of previous month.
const maxDaysInMonth = new Date(displayYear, displayMonth + 1, 0).getDate();
const displayDate = selectedDay == null ? 1 : Math.min(selectedDay, maxDaysInMonth);
// 12:00 matches the underlying react-day-picker timestamp behavior
const value = common_1.DateUtils.getDateTime(new Date(displayYear, displayMonth, displayDate, 12), this.state.value);
// clamp between min and max dates
if (value != null && value < minDate) {
return minDate;
}
else if (value != null && value > maxDate) {
return maxDate;
}
return value;
}
/**
* Update `value` by invoking `onChange` (always) and setting state (if uncontrolled).
*/
updateValue(value, isUserChange, skipOnChange = false) {
var _a, _b;
if (!skipOnChange) {
(_b = (_a = this.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, value, isUserChange);
}
if (this.props.value === undefined) {
this.setState({ value });
}
}
}
exports.DatePicker = DatePicker;
DatePicker.defaultProps = {
canClearSelection: true,
clearButtonText: "Clear",
dayPickerProps: {},
highlightCurrentDay: false,
locale: "en-US",
maxDate: datePickerUtils_1.DatePickerUtils.getDefaultMaxDate(),
minDate: datePickerUtils_1.DatePickerUtils.getDefaultMinDate(),
reverseMonthAndYearMenus: false,
shortcuts: false,
showActionsBar: false,
todayButtonText: "Today",
};
DatePicker.displayName = `${core_1.DISPLAYNAME_PREFIX}.DatePicker`;
function getInitialValue(props) {
// !== because `null` is a valid value (no date)
if (props.value !== undefined) {
return props.value;
}
if (props.defaultValue !== undefined) {
return props.defaultValue;
}
return null;
}
function getInitialMonth(props, value) {
var _a, _b;
const rangeFromProps = [(_a = props.minDate) !== null && _a !== void 0 ? _a : null, (_b = props.maxDate) !== null && _b !== void 0 ? _b : null];
const today = new Date();
// != because we must have a real `Date` to begin the calendar on.
if (props.initialMonth != null) {
return props.initialMonth;
}
else if (value != null) {
return value;
}
else if (common_1.DateUtils.isDayInRange(today, rangeFromProps)) {
return today;
}
else {
return common_1.DateUtils.getDateBetween(rangeFromProps);
}
}
function isTodayEnabled(minDate, maxDate) {
const today = new Date();
return common_1.DateUtils.isDayInRange(today, [minDate, maxDate]);
}
//# sourceMappingURL=datePicker.js.map