UNPKG

@blueprintjs/datetime

Version:

Components for interacting with dates and times

306 lines 15.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DatePicker = void 0; const tslib_1 = require("tslib"); const jsx_runtime_1 = require("react/jsx-runtime"); /* * 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. */ const classnames_1 = tslib_1.__importDefault(require("classnames")); const date_fns_1 = require("date-fns"); 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 dateConstants_1 = require("../dateConstants"); 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"); /** * Date picker component. * * @see https://blueprintjs.com/docs/#datetime/date-picker */ class DatePicker extends dateFnsLocalizedComponent_1.DateFnsLocalizedComponent { static defaultProps = { canClearSelection: true, clearButtonText: "Clear", dayPickerProps: {}, highlightCurrentDay: false, locale: dateConstants_1.LOCALE, maxDate: dateConstants_1.MAX_DATE, minDate: dateConstants_1.MIN_DATE, reverseMonthAndYearMenus: false, shortcuts: false, showActionsBar: false, todayButtonText: "Today", }; static displayName = `${core_1.DISPLAYNAME_PREFIX}.DatePicker`; ignoreNextMonthChange = false; constructor(props) { super(props); 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() { const { className, dayPickerProps, footerElement, maxDate, minDate, showActionsBar } = this.props; const { displayMonth, displayYear, locale } = this.state; return ((0, jsx_runtime_1.jsxs)("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, }), children: [this.maybeRenderShortcuts(), (0, jsx_runtime_1.jsx)("div", { className: common_1.Classes.DATEPICKER_CONTENT, children: (0, jsx_runtime_1.jsxs)(datePickerContext_1.DatePickerProvider, { ...this.props, ...this.state, children: [(0, jsx_runtime_1.jsx)(react_day_picker_1.DayPicker, { locale: locale, showOutsideDays: true, ...dayPickerProps, captionLayout: "dropdown-buttons", classNames: { ...classes_1.dayPickerClassNameOverrides, ...dayPickerProps?.classNames, }, components: { Dropdown: datePickerDropdown_1.DatePickerDropdown, IconLeft: datePickerNavIcons_1.IconLeft, IconRight: datePickerNavIcons_1.IconRight, ...dayPickerProps?.components, }, formatters: { formatWeekdayName: this.renderWeekdayName, ...dayPickerProps?.formatters, }, fromDate: minDate, mode: "single", month: new Date(displayYear, displayMonth), onMonthChange: this.handleMonthChange, onSelect: this.handleDaySelect, required: !this.props.canClearSelection, selected: this.state.value ?? 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); } } /** * 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. */ renderWeekdayName = date => { return (0, date_fns_1.format)(date, "EEEEEE", { locale: this.state.locale }); }; renderOptionsBar() { const { clearButtonText, todayButtonText, minDate, maxDate, canClearSelection } = this.props; const todayEnabled = isTodayEnabled(minDate, maxDate); return [ (0, jsx_runtime_1.jsx)(core_1.Divider, {}, "div"), (0, jsx_runtime_1.jsxs)("div", { className: common_1.Classes.DATEPICKER_FOOTER, children: [(0, jsx_runtime_1.jsx)(core_1.Button, { disabled: !todayEnabled, onClick: this.handleTodayClick, text: todayButtonText, variant: "minimal" }), (0, jsx_runtime_1.jsx)(core_1.Button, { disabled: !canClearSelection, onClick: this.handleClearClick, text: clearButtonText, variant: "minimal" })] }, "footer"), ]; } 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 ((0, jsx_runtime_1.jsx)("div", { className: common_1.Classes.DATEPICKER_TIMEPICKER_WRAPPER, children: (0, jsx_runtime_1.jsx)(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 = dateConstants_1.MAX_DATE, minDate = dateConstants_1.MIN_DATE, 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 [ (0, jsx_runtime_1.jsx)(shortcuts_1.DatePickerShortcutMenu, { allowSingleDayRange: true, maxDate: maxDate, minDate: minDate, selectedShortcutIndex: selectedShortcutIndex, shortcuts: dateRangeShortcuts, timePrecision: timePrecision, onShortcutClick: this.handleShortcutClick, useSingleDateShortcuts: true }, "shortcuts"), (0, jsx_runtime_1.jsx)(core_1.Divider, {}, "div"), ]; } handleDaySelect = (day, selectedDay, activeModifiers, e) => { if (activeModifiers.disabled) { return; } else if (day === undefined) { this.handleClearClick(); return; } this.updateDay(day); this.props.dayPickerProps?.onSelect?.(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); }; 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?.(datePickerShortcut, selectedShortcutIndex); }; 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; } }; 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; } handleClearClick = () => this.updateValue(null, true); handleMonthChange = (newDate) => { const date = this.computeValidDateInSpecifiedMonthYear(newDate.getFullYear(), newDate.getMonth()); if (date != null) { this.setState({ displayMonth: date.getMonth(), displayYear: date.getFullYear() }); this.props.dayPickerProps?.onMonthChange?.(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; } }; 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); }; handleTimeChange = (time) => { this.props.timePickerProps?.onChange?.(time); const { value } = this.state; const newValue = common_1.DateUtils.getDateTime(value != null ? value : new Date(), time); this.updateValue(newValue, true); }; /** * Update `value` by invoking `onChange` (always) and setting state (if uncontrolled). */ updateValue(value, isUserChange, skipOnChange = false) { if (!skipOnChange) { this.props.onChange?.(value, isUserChange); } if (this.props.value === undefined) { this.setState({ value }); } } } exports.DatePicker = 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) { const rangeFromProps = [props.minDate ?? null, props.maxDate ?? 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