UNPKG

@blueprintjs/datetime

Version:

Components for interacting with dates and times

317 lines 15.9 kB
/* * 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 { format } from "date-fns"; import * as React from "react"; import { DayPicker } from "react-day-picker"; import { Button, DISPLAYNAME_PREFIX, Divider } from "@blueprintjs/core"; import { Classes, DateUtils, Errors, TimezoneUtils } from "../../common"; import { dayPickerClassNameOverrides } from "../../common/classes"; import { DateFnsLocalizedComponent } from "../dateFnsLocalizedComponent"; import { DatePickerDropdown } from "../react-day-picker/datePickerDropdown"; import { IconLeft, IconRight } from "../react-day-picker/datePickerNavIcons"; import { DatePickerShortcutMenu } from "../shortcuts/shortcuts"; import { TimePicker } from "../time-picker/timePicker"; import { DatePickerProvider } from "./datePickerContext"; import { DatePickerUtils } from "./datePickerUtils"; /** * Date picker component. * * @see https://blueprintjs.com/docs/#datetime/date-picker */ export class DatePicker extends 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 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 : 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 : 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 ? 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 = 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: classNames(Classes.DATEPICKER, className, { [Classes.DATEPICKER3_HIGHLIGHT_CURRENT_DAY]: this.props.highlightCurrentDay, [Classes.DATEPICKER3_REVERSE_MONTH_AND_YEAR]: this.props.reverseMonthAndYearMenus, }) }, this.maybeRenderShortcuts(), React.createElement("div", { className: Classes.DATEPICKER_CONTENT }, React.createElement(DatePickerProvider, { ...this.props, ...this.state }, React.createElement(DayPicker, { locale: locale, showOutsideDays: true, ...dayPickerProps, captionLayout: "dropdown-buttons", classNames: { ...dayPickerClassNameOverrides, ...dayPickerProps === null || dayPickerProps === void 0 ? void 0 : dayPickerProps.classNames, }, components: { Dropdown: DatePickerDropdown, IconLeft, 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 && !DateUtils.isDayInRange(defaultValue, [minDate, maxDate])) { console.error(Errors.DATEPICKER_DEFAULT_VALUE_INVALID); } if (initialMonth != null && !DateUtils.isMonthInRange(initialMonth, [minDate, maxDate])) { console.error(Errors.DATEPICKER_INITIAL_MONTH_INVALID); } if (maxDate != null && minDate != null && maxDate < minDate && !DateUtils.isSameDay(maxDate, minDate)) { console.error(Errors.DATEPICKER_MAX_DATE_INVALID); } if (value != null && !DateUtils.isDayInRange(value, [minDate, maxDate])) { console.error(Errors.DATEPICKER_VALUE_INVALID); } } renderOptionsBar() { const { clearButtonText, todayButtonText, minDate, maxDate, canClearSelection } = this.props; const todayEnabled = isTodayEnabled(minDate, maxDate); return [ React.createElement(Divider, { key: "div" }), React.createElement("div", { className: Classes.DATEPICKER_FOOTER, key: "footer" }, React.createElement(Button, { disabled: !todayEnabled, onClick: this.handleTodayClick, text: todayButtonText, variant: "minimal" }), React.createElement(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 && DateUtils.isSameDay(this.state.value, minDate); const applyMax = this.state.value != null && DateUtils.isSameDay(this.state.value, maxDate); return (React.createElement("div", { className: Classes.DATEPICKER_TIMEPICKER_WRAPPER }, React.createElement(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.getDefaultMaxDate(), minDate = 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(DatePickerShortcutMenu, { key: "shortcuts", allowSingleDayRange: true, maxDate: maxDate, minDate: minDate, selectedShortcutIndex: selectedShortcutIndex, shortcuts: dateRangeShortcuts, timePrecision: timePrecision, onShortcutClick: this.handleShortcutClick, useSingleDateShortcuts: true }), React.createElement(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 = 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 }); } } } DatePicker.defaultProps = { canClearSelection: true, clearButtonText: "Clear", dayPickerProps: {}, highlightCurrentDay: false, locale: "en-US", maxDate: DatePickerUtils.getDefaultMaxDate(), minDate: DatePickerUtils.getDefaultMinDate(), reverseMonthAndYearMenus: false, shortcuts: false, showActionsBar: false, todayButtonText: "Today", }; DatePicker.displayName = `${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 (DateUtils.isDayInRange(today, rangeFromProps)) { return today; } else { return DateUtils.getDateBetween(rangeFromProps); } } function isTodayEnabled(minDate, maxDate) { const today = new Date(); return DateUtils.isDayInRange(today, [minDate, maxDate]); } //# sourceMappingURL=datePicker.js.map